[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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 168 additions and 190 deletions

View File

@ -5218,9 +5218,6 @@
"message": "to require all members to log in with SSO.",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'"
},
"ssoPolicyHelpKeyConnector": {
"message": "The require SSO authentication and single organization policies are required to set up Key Connector decryption."
},
"memberDecryptionOption": {
"message": "Member decryption options"
},
@ -5230,8 +5227,17 @@
"keyConnector": {
"message": "Key Connector"
},
"memberDecryptionKeyConnectorDesc": {
"message": "Connect login with SSO to your self-hosted decryption key server. Using this option, members wont need to use their master passwords to decrypt vault data. Contact Bitwarden Support for set up assistance."
"memberDecryptionKeyConnectorDescStart": {
"message": "Connect login with SSO to your self-hosted decryption key server. Using this option, members wont need to use their master passwords to decrypt vault data. The",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members wont need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'"
},
"memberDecryptionKeyConnectorDescLink": {
"message": "require SSO authentication and single organization policies",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members wont need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'"
},
"memberDecryptionKeyConnectorDescEnd": {
"message": "are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members wont need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'"
},
"keyConnectorPolicyRestriction": {
"message": "\"Login with SSO and Key Connector Decryption\" is activated. This policy will only apply to owners and admins."
@ -5535,7 +5541,7 @@
},
"lastSync": {
"message": "Last sync",
"Description": "Used as a prefix to indicate the last time a sync occured. Example \"Last sync 1968-11-16 00:00:00\""
"description": "Used as a prefix to indicate the last time a sync occured. Example \"Last sync 1968-11-16 00:00:00\""
},
"sponsorshipsSynced": {
"message": "Self-hosted sponsorships synced."
@ -6781,5 +6787,20 @@
},
"updateKdfSettings": {
"message": "Update KDF settings"
},
"trustedDeviceEncryption": {
"message": "Trusted device encryption"
},
"memberDecryptionTdeDescStart": {
"message": "Once authenticated, members will decrypt vault data using a key stored on their device. The",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The master password reset policy with automatic enrollment will turn on when this option is used.'"
},
"memberDecryptionTdeDescLink": {
"message": "master password reset policy",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The master password reset policy with automatic enrollment will turn on when this option is used.'"
},
"memberDecryptionTdeDescEnd": {
"message": "with automatic enrollment will turn on when this option is used.",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The master password reset policy with automatic enrollment will turn on when this option is used.'"
}
}

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)
);
}

View File

@ -4,6 +4,12 @@ export enum SsoType {
Saml2 = 2,
}
export enum MemberDecryptionType {
MasterPassword = 0,
KeyConnector = 1,
TrustedDeviceEncryption = 2,
}
export enum OpenIdConnectRedirectBehavior {
RedirectGet = 0,
FormPost = 1,

View File

@ -1,5 +1,6 @@
import { BaseResponse } from "../../../models/response/base.response";
import {
MemberDecryptionType,
OpenIdConnectRedirectBehavior,
Saml2BindingType,
Saml2NameIdFormat,
@ -11,8 +12,8 @@ import { SsoConfigView } from "../view/sso-config.view";
export class SsoConfigApi extends BaseResponse {
static fromView(view: SsoConfigView, api = new SsoConfigApi()) {
api.configType = view.configType;
api.memberDecryptionType = view.memberDecryptionType;
api.keyConnectorEnabled = view.keyConnectorEnabled;
api.keyConnectorUrl = view.keyConnectorUrl;
if (api.configType === SsoType.OpenIdConnect) {
@ -52,8 +53,8 @@ export class SsoConfigApi extends BaseResponse {
return api;
}
configType: SsoType;
memberDecryptionType: MemberDecryptionType;
keyConnectorEnabled: boolean;
keyConnectorUrl: string;
// OpenId
@ -95,8 +96,8 @@ export class SsoConfigApi extends BaseResponse {
}
this.configType = this.getResponseProperty("ConfigType");
this.memberDecryptionType = this.getResponseProperty("MemberDecryptionType");
this.keyConnectorEnabled = this.getResponseProperty("KeyConnectorEnabled");
this.keyConnectorUrl = this.getResponseProperty("KeyConnectorUrl");
this.authority = this.getResponseProperty("Authority");

View File

@ -1,5 +1,6 @@
import { View } from "../../../models/view/view";
import {
MemberDecryptionType,
OpenIdConnectRedirectBehavior,
Saml2BindingType,
Saml2NameIdFormat,
@ -14,7 +15,7 @@ export class SsoConfigView extends View {
configType: SsoType;
keyConnectorEnabled: boolean;
memberDecryptionType: MemberDecryptionType;
keyConnectorUrl: string;
openId: {
@ -66,8 +67,8 @@ export class SsoConfigView extends View {
}
this.configType = orgSsoResponse.data.configType;
this.memberDecryptionType = orgSsoResponse.data.memberDecryptionType;
this.keyConnectorEnabled = orgSsoResponse.data.keyConnectorEnabled;
this.keyConnectorUrl = orgSsoResponse.data.keyConnectorUrl;
if (this.configType === SsoType.OpenIdConnect) {

View File

@ -1,4 +1,5 @@
export enum FeatureFlag {
DisplayEuEnvironmentFlag = "display-eu-environment",
DisplayLowKdfIterationWarningFlag = "display-kdf-iteration-warning",
TrustedDeviceEncryption = "trusted-device-encryption",
}