1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 02:03:39 +00:00

[AC-1145] Add trusted devices option to encryption settings on sso config (#5383)

* [AC-1145] Add TDE feature flag

* [AC-1145] Update sso-config to use new member decryption type and remove keyConnectorEnabled

* [AC-1145] Add new TDE option to SSO config form and update to CL radio buttons

* [AC-1145] Update checkboxes to CL checkboxes

* [AC-1145] Fix messages.json warning

* [AC-1145] Update to new form async actions

* [AC-1145] Modify key connector option display logic to check for TDE feature flag

* [AC-1145] Remove obsolete app-checkbox component

* [AC-1145] Update TDE option description to refer to master password reset policy
This commit is contained in:
Shane Melton
2023-05-10 12:51:56 -07:00
committed by GitHub
parent a64cecff68
commit ab4d8df2ae
11 changed files with 168 additions and 190 deletions

View File

@@ -1,63 +0,0 @@
import { Directive, Input, OnInit, Self } from "@angular/core";
import { ControlValueAccessor, UntypedFormControl, NgControl, Validators } from "@angular/forms";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Directive()
export abstract class BaseCvaComponent implements ControlValueAccessor, OnInit {
get describedById() {
return this.showDescribedBy ? this.controlId + "Desc" : null;
}
get showDescribedBy() {
return this.helperText != null || this.controlDir.control.hasError("required");
}
get isRequired() {
return this.controlDir.control.hasValidator(Validators.required);
}
@Input() label: string;
@Input() controlId: string;
@Input() helperText: string;
internalControl = new UntypedFormControl("");
protected onChange: any;
protected onTouched: any;
constructor(@Self() public controlDir: NgControl) {
this.controlDir.valueAccessor = this;
}
ngOnInit() {
this.internalControl.valueChanges.subscribe(this.onValueChangesInternal);
}
onBlurInternal() {
this.onTouched();
}
// CVA interfaces
writeValue(value: string) {
this.internalControl.setValue(value);
}
registerOnChange(fn: any) {
this.onChange = fn;
}
registerOnTouched(fn: any) {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this.internalControl.disable();
} else {
this.internalControl.enable();
}
}
protected onValueChangesInternal: any = (value: string) => this.onChange(value);
// End CVA interfaces
}

View File

@@ -1,16 +0,0 @@
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[attr.id]="controlId"
[attr.aria-describedby]="describedById"
[formControl]="internalControl"
(blur)="onBlurInternal()"
/>
<label class="form-check-label" [attr.for]="controlId">{{ label }}</label>
</div>
<small *ngIf="showDescribedBy" [attr.id]="describedById" class="form-text text-muted">{{
helperText
}}</small>
</div>

View File

@@ -1,10 +0,0 @@
import { Component } from "@angular/core";
import { BaseCvaComponent } from "./base-cva.component";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-input-checkbox",
templateUrl: "input-checkbox.component.html",
})
export class InputCheckboxComponent extends BaseCvaComponent {}

View File

@@ -4,7 +4,6 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module";
import { SsoComponent } from "../../auth/sso/sso.component";
import { InputCheckboxComponent } from "./components/input-checkbox.component";
import { DomainAddEditDialogComponent } from "./manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component";
import { DomainVerificationComponent } from "./manage/domain-verification/domain-verification.component";
import { ScimComponent } from "./manage/scim.component";
@@ -13,7 +12,6 @@ import { OrganizationsRoutingModule } from "./organizations-routing.module";
@NgModule({
imports: [SharedModule, OrganizationsRoutingModule],
declarations: [
InputCheckboxComponent,
SsoComponent,
ScimComponent,
DomainVerificationComponent,

View File

@@ -8,32 +8,24 @@
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<form
#form
(ngSubmit)="submit()"
[formGroup]="ssoConfigForm"
[appApiAction]="formPromise"
*ngIf="!loading"
>
<form [formGroup]="ssoConfigForm" [bitSubmit]="submit" *ngIf="!loading">
<p>
{{ "ssoPolicyHelpStart" | i18n }}
<a routerLink="../policies">{{ "ssoPolicyHelpLink" | i18n }}</a>
{{ "ssoPolicyHelpEnd" | i18n }}
<br />
{{ "ssoPolicyHelpKeyConnector" | i18n }}
</p>
<!-- Root form -->
<ng-container>
<app-input-checkbox
controlId="enabled"
formControlName="enabled"
[label]="'allowSso' | i18n"
[helperText]="'allowSsoDesc' | i18n"
></app-input-checkbox>
<bit-form-control>
<bit-label>{{ "allowSso" | i18n }}</bit-label>
<input bitCheckbox type="checkbox" formControlName="enabled" id="enabled" />
<bit-hint>{{ "allowSsoDesc" | i18n }}</bit-hint>
</bit-form-control>
<bit-form-field>
<bit-label>{{ "ssoIdentifier" | i18n }}</bit-label>
@@ -43,31 +35,25 @@
<hr />
<div class="form-group">
<label>{{ "memberDecryptionOption" | i18n }}</label>
<div class="form-check form-check-block">
<input
class="form-check-input"
type="radio"
id="memberDecryptionPass"
[value]="false"
formControlName="keyConnectorEnabled"
/>
<label class="form-check-label" for="memberDecryptionPass">
{{ "masterPass" | i18n }}
<small>{{ "memberDecryptionPassDesc" | i18n }}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
id="memberDecryptionKey"
[value]="true"
formControlName="keyConnectorEnabled"
[attr.disabled]="!organization.useKeyConnector || null"
/>
<label class="form-check-label" for="memberDecryptionKey">
<bit-radio-group formControlName="memberDecryptionType">
<bit-label>{{ "memberDecryptionOption" | i18n }}</bit-label>
<bit-radio-button
class="tw-block"
id="memberDecryptionPass"
[value]="memberDecryptionType.MasterPassword"
>
<bit-label>{{ "masterPass" | i18n }}</bit-label>
</bit-radio-button>
<bit-radio-button
class="tw-block"
id="memberDecryptionKey"
[value]="memberDecryptionType.KeyConnector"
[disabled]="!organization.useKeyConnector || null"
*ngIf="showKeyConnectorOptions"
>
<bit-label>
{{ "keyConnector" | i18n }}
<a
target="_blank"
@@ -77,13 +63,38 @@
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
<small>{{ "memberDecryptionKeyConnectorDesc" | i18n }}</small>
</label>
</div>
</div>
</bit-label>
<bit-hint>
{{ "memberDecryptionKeyConnectorDescStart" | i18n }}
<a routerLink="../policies">{{ "memberDecryptionKeyConnectorDescLink" | i18n }}</a>
{{ "memberDecryptionKeyConnectorDescEnd" | i18n }}
</bit-hint>
</bit-radio-button>
<bit-radio-button
class="tw-block"
id="memberDecryptionTde"
[value]="memberDecryptionType.TrustedDeviceEncryption"
*ngIf="showTdeOptions"
>
<bit-label>
{{ "trustedDeviceEncryption" | i18n }}
</bit-label>
<bit-hint>
{{ "memberDecryptionTdeDescStart" | i18n }}
<a routerLink="../policies">{{ "memberDecryptionTdeDescLink" | i18n }}</a>
{{ "memberDecryptionTdeDescEnd" | i18n }}
</bit-hint>
</bit-radio-button>
</bit-radio-group>
<!-- Key Connector -->
<ng-container *ngIf="ssoConfigForm.get('keyConnectorEnabled').value">
<ng-container
*ngIf="
ssoConfigForm.value.memberDecryptionType === memberDecryptionType.KeyConnector &&
showKeyConnectorOptions
"
>
<app-callout type="warning" [useAlertRole]="true">
{{ "keyConnectorWarning" | i18n }}
</app-callout>
@@ -205,11 +216,15 @@
</select>
</bit-form-field>
<app-input-checkbox
controlId="getClaimsFromUserInfoEndpoint"
formControlName="getClaimsFromUserInfoEndpoint"
[label]="'getClaimsFromUserInfoEndpoint' | i18n"
></app-input-checkbox>
<bit-form-control>
<bit-label>{{ "getClaimsFromUserInfoEndpoint" | i18n }}</bit-label>
<input
bitCheckbox
type="checkbox"
formControlName="getClaimsFromUserInfoEndpoint"
id="getClaimsFromUserInfoEndpoint"
/>
</bit-form-control>
<!-- Optional customizations -->
<div
@@ -381,17 +396,25 @@
</select>
</bit-form-field>
<app-input-checkbox
controlId="spWantAssertionsSigned"
formControlName="spWantAssertionsSigned"
[label]="'spWantAssertionsSigned' | i18n"
></app-input-checkbox>
<bit-form-control>
<bit-label>{{ "spWantAssertionsSigned" | i18n }}</bit-label>
<input
bitCheckbox
type="checkbox"
formControlName="spWantAssertionsSigned"
id="spWantAssertionsSigned"
/>
</bit-form-control>
<app-input-checkbox
controlId="spValidateCertificates"
formControlName="spValidateCertificates"
[label]="'spValidateCertificates' | i18n"
></app-input-checkbox>
<bit-form-control>
<bit-label>{{ "spValidateCertificates" | i18n }}</bit-label>
<input
bitCheckbox
type="checkbox"
formControlName="spValidateCertificates"
id="spValidateCertificates"
/>
</bit-form-control>
</div>
<!-- SAML2 IDP -->
@@ -462,21 +485,29 @@
[label]="'idpAllowUnsolicitedAuthnResponse' | i18n"
></app-input-checkbox> -->
<app-input-checkbox
controlId="idpAllowOutboundLogoutRequests"
formControlName="idpAllowOutboundLogoutRequests"
[label]="'idpAllowOutboundLogoutRequests' | i18n"
></app-input-checkbox>
<bit-form-control>
<bit-label>{{ "idpAllowOutboundLogoutRequests" | i18n }}</bit-label>
<input
bitCheckbox
type="checkbox"
formControlName="idpAllowOutboundLogoutRequests"
id="idpAllowOutboundLogoutRequests"
/>
</bit-form-control>
<app-input-checkbox
controlId="idpWantAuthnRequestsSigned"
formControlName="idpWantAuthnRequestsSigned"
[label]="'idpSignAuthenticationRequests' | i18n"
></app-input-checkbox>
<bit-form-control>
<bit-label>{{ "idpSignAuthenticationRequests" | i18n }}</bit-label>
<input
bitCheckbox
type="checkbox"
formControlName="idpWantAuthnRequestsSigned"
id="idpWantAuthnRequestsSigned"
/>
</bit-form-control>
</div>
</div>
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
<button type="submit" buttonType="primary" bitButton bitFormButton>
{{ "save" | i18n }}
</button>
<bit-error-summary [formGroup]="ssoConfigForm"></bit-error-summary>

View File

@@ -12,12 +12,14 @@ import { concatMap, Subject, takeUntil } from "rxjs";
import { SelectOptions } from "@bitwarden/angular/interfaces/selectOptions";
import { ControlsOf } from "@bitwarden/angular/types/controls-of";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ConfigServiceAbstraction } from "@bitwarden/common/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import {
MemberDecryptionType,
OpenIdConnectRedirectBehavior,
Saml2BindingType,
Saml2NameIdFormat,
@@ -28,6 +30,7 @@ import { SsoConfigApi } from "@bitwarden/common/auth/models/api/sso-config.api";
import { OrganizationSsoRequest } from "@bitwarden/common/auth/models/request/organization-sso.request";
import { OrganizationSsoResponse } from "@bitwarden/common/auth/models/response/organization-sso.response";
import { SsoConfigView } from "@bitwarden/common/auth/models/view/sso-config.view";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { Utils } from "@bitwarden/common/misc/utils";
import { ssoTypeValidator } from "./sso-type.validator";
@@ -40,6 +43,7 @@ const defaultSigningAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha2
})
export class SsoComponent implements OnInit, OnDestroy {
readonly ssoType = SsoType;
readonly memberDecryptionType = MemberDecryptionType;
readonly ssoTypeOptions: SelectOptions[] = [
{ name: this.i18nService.t("selectType"), value: SsoType.None, disabled: true },
@@ -83,6 +87,8 @@ export class SsoComponent implements OnInit, OnDestroy {
];
private destroy$ = new Subject<void>();
showTdeOptions = false;
showKeyConnectorOptions = false;
showOpenIdCustomizations = false;
@@ -90,7 +96,6 @@ export class SsoComponent implements OnInit, OnDestroy {
haveTestedKeyConnector = false;
organizationId: string;
organization: Organization;
formPromise: Promise<OrganizationSsoResponse>;
callbackPath: string;
signedOutCallbackPath: string;
@@ -147,7 +152,7 @@ export class SsoComponent implements OnInit, OnDestroy {
protected ssoConfigForm = this.formBuilder.group<ControlsOf<SsoConfigView>>({
configType: new FormControl(SsoType.None),
keyConnectorEnabled: new FormControl(false),
memberDecryptionType: new FormControl(MemberDecryptionType.MasterPassword),
keyConnectorUrl: new FormControl(""),
openId: this.openIdForm,
saml: this.samlForm,
@@ -174,7 +179,8 @@ export class SsoComponent implements OnInit, OnDestroy {
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction
private organizationApiService: OrganizationApiServiceAbstraction,
private configService: ConfigServiceAbstraction
) {}
async ngOnInit() {
@@ -223,6 +229,15 @@ export class SsoComponent implements OnInit, OnDestroy {
takeUntil(this.destroy$)
)
.subscribe();
const tdeFeatureFlag = await this.configService.getFeatureFlagBool(
FeatureFlag.TrustedDeviceEncryption
);
this.showTdeOptions = tdeFeatureFlag && !this.platformUtilsService.isSelfHost();
// If the tde flag is not enabled, continue showing the key connector options to keep the UI the same
// Once the flag is removed, we can rely on the platformUtilsService.isSelfHost() check alone
this.showKeyConnectorOptions = !tdeFeatureFlag || this.platformUtilsService.isSelfHost();
}
ngOnDestroy(): void {
@@ -244,10 +259,10 @@ export class SsoComponent implements OnInit, OnDestroy {
this.loading = false;
}
async submit() {
submit = async () => {
this.updateFormValidationState(this.ssoConfigForm);
if (this.ssoConfigForm.value.keyConnectorEnabled) {
if (this.ssoConfigForm.value.memberDecryptionType === MemberDecryptionType.KeyConnector) {
this.haveTestedKeyConnector = false;
await this.validateKeyConnectorUrl();
}
@@ -262,18 +277,11 @@ export class SsoComponent implements OnInit, OnDestroy {
request.identifier = this.ssoIdentifierCtrl.value === "" ? null : this.ssoIdentifierCtrl.value;
request.data = SsoConfigApi.fromView(this.ssoConfigForm.getRawValue());
this.formPromise = this.organizationApiService.updateSso(this.organizationId, request);
const response = await this.organizationApiService.updateSso(this.organizationId, request);
this.populateForm(response);
try {
const response = await this.formPromise;
this.populateForm(response);
this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved"));
} catch {
// Logged by appApiAction, do nothing
}
this.formPromise = null;
}
this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved"));
};
async validateKeyConnectorUrl() {
if (this.haveTestedKeyConnector) {
@@ -313,7 +321,7 @@ export class SsoComponent implements OnInit, OnDestroy {
get enableTestKeyConnector() {
return (
this.ssoConfigForm.get("keyConnectorEnabled").value &&
this.ssoConfigForm.value?.memberDecryptionType === MemberDecryptionType.KeyConnector &&
!Utils.isNullOrWhitespace(this.keyConnectorUrl?.value)
);
}