diff --git a/bitwarden_license/src/app/organizations/components/base-cva.component.ts b/bitwarden_license/src/app/organizations/components/base-cva.component.ts new file mode 100644 index 0000000000..409090ddc4 --- /dev/null +++ b/bitwarden_license/src/app/organizations/components/base-cva.component.ts @@ -0,0 +1,68 @@ +import { Directive, Input, OnInit, Self } from "@angular/core"; +import { ControlValueAccessor, FormControl, NgControl, Validators } from "@angular/forms"; + +import { dirtyRequired } from "jslib-angular/validators/dirty.validator"; + +/** 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) || + this.controlDir.control.hasValidator(dirtyRequired) + ); + } + + @Input() label: string; + @Input() controlId: string; + @Input() helperText: string; + + internalControl = new FormControl(""); + + 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 +} diff --git a/bitwarden_license/src/app/organizations/components/input-checkbox.component.html b/bitwarden_license/src/app/organizations/components/input-checkbox.component.html new file mode 100644 index 0000000000..2c3c8639c1 --- /dev/null +++ b/bitwarden_license/src/app/organizations/components/input-checkbox.component.html @@ -0,0 +1,16 @@ +
+
+ + +
+ {{ + helperText + }} +
diff --git a/bitwarden_license/src/app/organizations/components/input-checkbox.component.ts b/bitwarden_license/src/app/organizations/components/input-checkbox.component.ts new file mode 100644 index 0000000000..b494c6c817 --- /dev/null +++ b/bitwarden_license/src/app/organizations/components/input-checkbox.component.ts @@ -0,0 +1,10 @@ +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 {} diff --git a/bitwarden_license/src/app/organizations/components/input-text-readonly.component.html b/bitwarden_license/src/app/organizations/components/input-text-readonly.component.html new file mode 100644 index 0000000000..b25edf6363 --- /dev/null +++ b/bitwarden_license/src/app/organizations/components/input-text-readonly.component.html @@ -0,0 +1,26 @@ +
+ +
+ +
+ +
+
+ +
+
+
diff --git a/bitwarden_license/src/app/organizations/components/input-text-readonly.component.ts b/bitwarden_license/src/app/organizations/components/input-text-readonly.component.ts new file mode 100644 index 0000000000..a7618ac8e2 --- /dev/null +++ b/bitwarden_license/src/app/organizations/components/input-text-readonly.component.ts @@ -0,0 +1,25 @@ +import { Component, Input } from "@angular/core"; + +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; + +/** For use in the SSO Config Form only - will be deprecated by the Component Library */ +@Component({ + selector: "app-input-text-readonly", + templateUrl: "input-text-readonly.component.html", +}) +export class InputTextReadOnlyComponent { + @Input() controlValue: string; + @Input() label: string; + @Input() showCopy = true; + @Input() showLaunch = false; + + constructor(private platformUtilsService: PlatformUtilsService) {} + + copy(value: string) { + this.platformUtilsService.copyToClipboard(value); + } + + launchUri(url: string) { + this.platformUtilsService.launchUri(url); + } +} diff --git a/bitwarden_license/src/app/organizations/components/input-text.component.html b/bitwarden_license/src/app/organizations/components/input-text.component.html new file mode 100644 index 0000000000..e19963474c --- /dev/null +++ b/bitwarden_license/src/app/organizations/components/input-text.component.html @@ -0,0 +1,33 @@ +
+ + +
+ + {{ helperText }} + + + + {{ "error" | i18n }}: + {{ + controlDir.control.hasError(helperTextSameAsError) + ? helperText + : ("fieldRequiredError" | i18n: label) + }} + +
+
diff --git a/bitwarden_license/src/app/organizations/components/input-text.component.ts b/bitwarden_license/src/app/organizations/components/input-text.component.ts new file mode 100644 index 0000000000..a8810e3806 --- /dev/null +++ b/bitwarden_license/src/app/organizations/components/input-text.component.ts @@ -0,0 +1,48 @@ +import { Component, Input, OnInit } 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-text[label][controlId]", + templateUrl: "input-text.component.html", +}) +export class InputTextComponent extends BaseCvaComponent implements OnInit { + @Input() helperTextSameAsError: string; + @Input() requiredErrorMessage: string; + @Input() stripSpaces = false; + + transformValue: (value: string) => string = null; + + ngOnInit() { + super.ngOnInit(); + if (this.stripSpaces) { + this.transformValue = this.doStripSpaces; + } + } + + writeValue(value: string) { + this.internalControl.setValue(value == null ? "" : value); + } + + protected onValueChangesInternal: any = (value: string) => { + let newValue = value; + if (this.transformValue != null) { + newValue = this.transformValue(value); + this.internalControl.setValue(newValue, { emitEvent: false }); + } + this.onChange(newValue); + }; + + protected onValueChangeInternal(value: string) { + let newValue = value; + if (this.transformValue != null) { + newValue = this.transformValue(value); + this.internalControl.setValue(newValue, { emitEvent: false }); + } + } + + private doStripSpaces(value: string) { + return value.replace(/ /g, ""); + } +} diff --git a/bitwarden_license/src/app/organizations/components/select.component.html b/bitwarden_license/src/app/organizations/components/select.component.html new file mode 100644 index 0000000000..bc2108e8af --- /dev/null +++ b/bitwarden_license/src/app/organizations/components/select.component.html @@ -0,0 +1,19 @@ +
+ + +
diff --git a/bitwarden_license/src/app/organizations/components/select.component.ts b/bitwarden_license/src/app/organizations/components/select.component.ts new file mode 100644 index 0000000000..0045032865 --- /dev/null +++ b/bitwarden_license/src/app/organizations/components/select.component.ts @@ -0,0 +1,14 @@ +import { Component, Input } from "@angular/core"; + +import { SelectOptions } from "jslib-angular/interfaces/selectOptions"; + +import { BaseCvaComponent } from "./base-cva.component"; + +/** For use in the SSO Config Form only - will be deprecated by the Component Library */ +@Component({ + selector: "app-select", + templateUrl: "select.component.html", +}) +export class SelectComponent extends BaseCvaComponent { + @Input() selectOptions: SelectOptions[]; +} diff --git a/bitwarden_license/src/app/organizations/manage/sso.component.html b/bitwarden_license/src/app/organizations/manage/sso.component.html index 7a0a6d2761..525dc22b8e 100644 --- a/bitwarden_license/src/app/organizations/manage/sso.component.html +++ b/bitwarden_license/src/app/organizations/manage/sso.component.html @@ -14,10 +14,9 @@

{{ "ssoPolicyHelpStart" | i18n }} @@ -27,451 +26,407 @@ {{ "ssoPolicyHelpKeyConnector" | i18n }}

-
-
- - -
- {{ "allowSsoDesc" | i18n }} -
- -
- -
- - -
-
- - -
-
- - - - {{ "keyConnectorWarning" | i18n }} - + + +
- -
+ +
-
-
+
+ + +
+
+ + + + + {{ "keyConnectorWarning" | i18n }} + + +
+ +
+ +
+ +
+
+
+ + + {{ "error" | i18n }}: + {{ "keyConnectorTestFail" | i18n }} + + + + + {{ "keyConnectorTestSuccess" | i18n }} + +
- - - - -
+ + + + -
- - -
- -
+
-

{{ "openIdConnectConfig" | i18n }}

-
- -
- -
- -
-
+

{{ "openIdConnectConfig" | i18n }}

+ + + + + + + + + + + + + + + + + + + +
+

+ {{ "openIdOptionalCustomizations" | i18n }} +

+
-
- -
- -
- -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
- - -
-
- - + + + -
-
- - + + -
-
- - + + -
-
- - -
-
- - + + + + + >
-
+ +
-

{{ "samlSpConfig" | i18n }}

-
- -
- -
- -
-
-
-
- -
- -
- - -
-
-
-
- -
- -
- -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
-
- - -
-
+

{{ "samlSpConfig" | i18n }}

+ + + + + + + + + + + + + + + + + + + + + +
-

{{ "samlIdpConfig" | i18n }}

+

{{ "samlIdpConfig" | i18n }}

+ + + + + + + + +
- - -
-
- - -
-
- - -
-
- - -
-
- + -
-
- - -
-
- -
- - -
-
-
-
- - -
-
-
-
- - -
+ + {{ "error" | i18n }}: + {{ "fieldRequiredError" | i18n: ("idpX509PublicCert" | i18n) }} +
+ + + + + + + + + +
@@ -479,4 +434,15 @@ {{ "save" | i18n }} +
+ + {{ "error" | i18n }}: + {{ + (errorCount === 1 ? "formErrorSummarySingle" : "formErrorSummaryPlural") | i18n: errorCount + }} +
diff --git a/bitwarden_license/src/app/organizations/manage/sso.component.ts b/bitwarden_license/src/app/organizations/manage/sso.component.ts index 392e07339a..8dc074a701 100644 --- a/bitwarden_license/src/app/organizations/manage/sso.component.ts +++ b/bitwarden_license/src/app/organizations/manage/sso.component.ts @@ -1,27 +1,82 @@ import { Component, OnInit } from "@angular/core"; -import { FormBuilder } from "@angular/forms"; +import { AbstractControl, FormBuilder, FormGroup } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; +import { SelectOptions } from "jslib-angular/interfaces/selectOptions"; +import { dirtyRequired } from "jslib-angular/validators/dirty.validator"; import { ApiService } from "jslib-common/abstractions/api.service"; import { I18nService } from "jslib-common/abstractions/i18n.service"; import { OrganizationService } from "jslib-common/abstractions/organization.service"; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { + OpenIdConnectRedirectBehavior, + Saml2BindingType, + Saml2NameIdFormat, + Saml2SigningBehavior, + SsoType, +} from "jslib-common/enums/ssoEnums"; +import { Utils } from "jslib-common/misc/utils"; +import { SsoConfigApi } from "jslib-common/models/api/ssoConfigApi"; import { Organization } from "jslib-common/models/domain/organization"; import { OrganizationSsoRequest } from "jslib-common/models/request/organization/organizationSsoRequest"; +import { OrganizationSsoResponse } from "jslib-common/models/response/organization/organizationSsoResponse"; +import { SsoConfigView } from "jslib-common/models/view/ssoConfigView"; + +const defaultSigningAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; @Component({ selector: "app-org-manage-sso", templateUrl: "sso.component.html", }) export class SsoComponent implements OnInit { - samlSigningAlgorithms = [ + readonly ssoType = SsoType; + + readonly ssoTypeOptions: SelectOptions[] = [ + { name: this.i18nService.t("selectType"), value: SsoType.None, disabled: true }, + { name: "OpenID Connect", value: SsoType.OpenIdConnect }, + { name: "SAML 2.0", value: SsoType.Saml2 }, + ]; + + readonly samlSigningAlgorithms = [ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "http://www.w3.org/2000/09/xmldsig#rsa-sha384", "http://www.w3.org/2000/09/xmldsig#rsa-sha512", "http://www.w3.org/2000/09/xmldsig#rsa-sha1", ]; + readonly saml2SigningBehaviourOptions: SelectOptions[] = [ + { + name: "If IdP Wants Authn Requests Signed", + value: Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned, + }, + { name: "Always", value: Saml2SigningBehavior.Always }, + { name: "Never", value: Saml2SigningBehavior.Never }, + ]; + readonly saml2BindingTypeOptions: SelectOptions[] = [ + { name: "Redirect", value: Saml2BindingType.HttpRedirect }, + { name: "HTTP POST", value: Saml2BindingType.HttpPost }, + ]; + readonly saml2NameIdFormatOptions: SelectOptions[] = [ + { name: "Not Configured", value: Saml2NameIdFormat.NotConfigured }, + { name: "Unspecified", value: Saml2NameIdFormat.Unspecified }, + { name: "Email Address", value: Saml2NameIdFormat.EmailAddress }, + { name: "X.509 Subject Name", value: Saml2NameIdFormat.X509SubjectName }, + { name: "Windows Domain Qualified Name", value: Saml2NameIdFormat.WindowsDomainQualifiedName }, + { name: "Kerberos Principal Name", value: Saml2NameIdFormat.KerberosPrincipalName }, + { name: "Entity Identifier", value: Saml2NameIdFormat.EntityIdentifier }, + { name: "Persistent", value: Saml2NameIdFormat.Persistent }, + { name: "Transient", value: Saml2NameIdFormat.Transient }, + ]; + + readonly connectRedirectOptions: SelectOptions[] = [ + { name: "Redirect GET", value: OpenIdConnectRedirectBehavior.RedirectGet }, + { name: "Form POST", value: OpenIdConnectRedirectBehavior.FormPost }, + ]; + + showOpenIdCustomizations = false; + loading = true; + haveTestedKeyConnector = false; organizationId: string; organization: Organization; formPromise: Promise; @@ -33,43 +88,57 @@ export class SsoComponent implements OnInit { spAcsUrl: string; enabled = this.formBuilder.control(false); - data = this.formBuilder.group({ - configType: [], - keyConnectorEnabled: [], - keyConnectorUrl: [], + openIdForm = this.formBuilder.group( + { + authority: ["", dirtyRequired], + clientId: ["", dirtyRequired], + clientSecret: ["", dirtyRequired], + metadataAddress: [], + redirectBehavior: [OpenIdConnectRedirectBehavior.RedirectGet, dirtyRequired], + getClaimsFromUserInfoEndpoint: [], + additionalScopes: [], + additionalUserIdClaimTypes: [], + additionalEmailClaimTypes: [], + additionalNameClaimTypes: [], + acrValues: [], + expectedReturnAcrValue: [], + }, + { + updateOn: "blur", + } + ); - // OpenId - authority: [], - clientId: [], - clientSecret: [], - metadataAddress: [], - redirectBehavior: [], - getClaimsFromUserInfoEndpoint: [], - additionalScopes: [], - additionalUserIdClaimTypes: [], - additionalEmailClaimTypes: [], - additionalNameClaimTypes: [], - acrValues: [], - expectedReturnAcrValue: [], + samlForm = this.formBuilder.group( + { + spNameIdFormat: [Saml2NameIdFormat.NotConfigured], + spOutboundSigningAlgorithm: [defaultSigningAlgorithm], + spSigningBehavior: [Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned], + spMinIncomingSigningAlgorithm: [defaultSigningAlgorithm], + spWantAssertionsSigned: [], + spValidateCertificates: [], - // SAML - spNameIdFormat: [], - spOutboundSigningAlgorithm: [], - spSigningBehavior: [], - spMinIncomingSigningAlgorithm: [], - spWantAssertionsSigned: [], - spValidateCertificates: [], + idpEntityId: ["", dirtyRequired], + idpBindingType: [Saml2BindingType.HttpRedirect], + idpSingleSignOnServiceUrl: [], + idpSingleLogoutServiceUrl: [], + idpX509PublicCert: ["", dirtyRequired], + idpOutboundSigningAlgorithm: [defaultSigningAlgorithm], + idpAllowUnsolicitedAuthnResponse: [], + idpAllowOutboundLogoutRequests: [true], + idpWantAuthnRequestsSigned: [], + }, + { + updateOn: "blur", + } + ); - idpEntityId: [], - idpBindingType: [], - idpSingleSignOnServiceUrl: [], - idpSingleLogoutServiceUrl: [], - idpX509PublicCert: [], - idpOutboundSigningAlgorithm: [], - idpAllowUnsolicitedAuthnResponse: [], - idpDisableOutboundLogoutRequests: [], - idpWantAuthnRequestsSigned: [], + ssoConfigForm = this.formBuilder.group({ + configType: [SsoType.None], + keyConnectorEnabled: [false], + keyConnectorUrl: [""], + openId: this.openIdForm, + saml: this.samlForm, }); constructor( @@ -82,6 +151,25 @@ export class SsoComponent implements OnInit { ) {} async ngOnInit() { + this.ssoConfigForm.get("configType").valueChanges.subscribe((newType: SsoType) => { + if (newType === SsoType.OpenIdConnect) { + this.openIdForm.enable(); + this.samlForm.disable(); + } else if (newType === SsoType.Saml2) { + this.openIdForm.disable(); + this.samlForm.enable(); + } else { + this.openIdForm.disable(); + this.samlForm.disable(); + } + }); + + this.samlForm + .get("spSigningBehavior") + .valueChanges.subscribe(() => + this.samlForm.get("idpX509PublicCert").updateValueAndValidity() + ); + this.route.parent.parent.params.subscribe(async (params) => { this.organizationId = params.organizationId; await this.load(); @@ -91,9 +179,7 @@ export class SsoComponent implements OnInit { async load() { this.organization = await this.organizationService.get(this.organizationId); const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId); - - this.data.patchValue(ssoSettings.data); - this.enabled.setValue(ssoSettings.enabled); + this.populateForm(ssoSettings); this.callbackPath = ssoSettings.urls.callbackPath; this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath; @@ -101,28 +187,30 @@ export class SsoComponent implements OnInit { this.spMetadataUrl = ssoSettings.urls.spMetadataUrl; this.spAcsUrl = ssoSettings.urls.spAcsUrl; - this.keyConnectorUrl.markAsDirty(); - this.loading = false; } - copy(value: string) { - this.platformUtilsService.copyToClipboard(value); - } - - launchUri(url: string) { - this.platformUtilsService.launchUri(url); - } - async submit() { - this.formPromise = this.postData(); + this.validateForm(this.ssoConfigForm); + + if (this.ssoConfigForm.get("keyConnectorEnabled").value) { + await this.validateKeyConnectorUrl(); + } + + if (!this.ssoConfigForm.valid) { + this.readOutErrors(); + return; + } + + const request = new OrganizationSsoRequest(); + request.enabled = this.enabled.value; + request.data = SsoConfigApi.fromView(this.ssoConfigForm.value as SsoConfigView); + + this.formPromise = this.apiService.postOrganizationSso(this.organizationId, request); try { const response = await this.formPromise; - - this.data.patchValue(response.data); - this.enabled.setValue(response.enabled); - + this.populateForm(response); this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved")); } catch { // Logged by appApiAction, do nothing @@ -131,24 +219,8 @@ export class SsoComponent implements OnInit { this.formPromise = null; } - async postData() { - if (this.data.get("keyConnectorEnabled").value) { - await this.validateKeyConnectorUrl(); - - if (this.keyConnectorUrl.hasError("invalidUrl")) { - throw new Error(this.i18nService.t("keyConnectorTestFail")); - } - } - - const request = new OrganizationSsoRequest(); - request.enabled = this.enabled.value; - request.data = this.data.value; - - return this.apiService.postOrganizationSso(this.organizationId, request); - } - async validateKeyConnectorUrl() { - if (this.keyConnectorUrl.pristine) { + if (this.haveTestedKeyConnector) { return; } @@ -163,18 +235,84 @@ export class SsoComponent implements OnInit { }); } - this.keyConnectorUrl.markAsPristine(); + this.haveTestedKeyConnector = true; + } + + toggleOpenIdCustomizations() { + this.showOpenIdCustomizations = !this.showOpenIdCustomizations; + } + + getErrorCount(form: FormGroup): number { + return Object.values(form.controls).reduce((acc: number, control: AbstractControl) => { + if (control instanceof FormGroup) { + return acc + this.getErrorCount(control); + } + + if (control.errors == null) { + return acc; + } + return acc + Object.keys(control.errors).length; + }, 0); } get enableTestKeyConnector() { return ( - this.data.get("keyConnectorEnabled").value && - this.keyConnectorUrl != null && - this.keyConnectorUrl.value !== "" + this.ssoConfigForm.get("keyConnectorEnabled").value && + !Utils.isNullOrWhitespace(this.keyConnectorUrl?.value) ); } get keyConnectorUrl() { - return this.data.get("keyConnectorUrl"); + return this.ssoConfigForm.get("keyConnectorUrl"); + } + + get samlSigningAlgorithmOptions(): SelectOptions[] { + return this.samlSigningAlgorithms.map((algorithm) => ({ name: algorithm, value: algorithm })); + } + + private validateForm(form: FormGroup) { + Object.values(form.controls).forEach((control: AbstractControl) => { + if (control.disabled) { + return; + } + + if (control instanceof FormGroup) { + this.validateForm(control); + } else { + control.markAsDirty(); + control.markAsTouched(); + control.updateValueAndValidity(); + } + }); + } + + private populateForm(ssoSettings: OrganizationSsoResponse) { + this.enabled.setValue(ssoSettings.enabled); + if (ssoSettings.data != null) { + const ssoConfigView = new SsoConfigView(ssoSettings.data); + this.ssoConfigForm.patchValue(ssoConfigView); + } + } + + private readOutErrors() { + const errorText = this.i18nService.t("error"); + const errorCount = this.getErrorCount(this.ssoConfigForm); + const errorCountText = this.i18nService.t( + errorCount === 1 ? "formErrorSummarySingle" : "formErrorSummaryPlural", + errorCount.toString() + ); + + const div = document.createElement("div"); + div.className = "sr-only"; + div.id = "srErrorCount"; + div.setAttribute("aria-live", "polite"); + div.innerText = errorText + ": " + errorCountText; + + const existing = document.getElementById("srErrorCount"); + if (existing != null) { + existing.remove(); + } + + document.body.append(div); } } diff --git a/bitwarden_license/src/app/organizations/organizations.module.ts b/bitwarden_license/src/app/organizations/organizations.module.ts index e81c270fab..838e8d4b29 100644 --- a/bitwarden_license/src/app/organizations/organizations.module.ts +++ b/bitwarden_license/src/app/organizations/organizations.module.ts @@ -4,11 +4,23 @@ import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { OssModule } from "src/app/oss.module"; +import { InputCheckboxComponent } from "./components/input-checkbox.component"; +import { InputTextReadOnlyComponent } from "./components/input-text-readonly.component"; +import { InputTextComponent } from "./components/input-text.component"; +import { SelectComponent } from "./components/select.component"; import { SsoComponent } from "./manage/sso.component"; import { OrganizationsRoutingModule } from "./organizations-routing.module"; +// Form components are for use in the SSO Configuration Form only and should not be exported for use elsewhere. +// They will be deprecated by the Component Library. @NgModule({ imports: [CommonModule, FormsModule, ReactiveFormsModule, OssModule, OrganizationsRoutingModule], - declarations: [SsoComponent], + declarations: [ + InputCheckboxComponent, + InputTextComponent, + InputTextReadOnlyComponent, + SelectComponent, + SsoComponent, + ], }) export class OrganizationsModule {} diff --git a/jslib b/jslib index a69135ce06..adfc2f234d 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit a69135ce066ed353424e3a3ef80dbfe4179224fe +Subproject commit adfc2f234d146e80695623af768b15d2616df8c4 diff --git a/src/app/oss.module.ts b/src/app/oss.module.ts index 38570162f5..b8eaeb0da0 100644 --- a/src/app/oss.module.ts +++ b/src/app/oss.module.ts @@ -61,12 +61,14 @@ import { CalloutComponent } from "jslib-angular/components/callout.component"; import { ExportScopeCalloutComponent } from "jslib-angular/components/export-scope-callout.component"; import { IconComponent } from "jslib-angular/components/icon.component"; import { VerifyMasterPasswordComponent } from "jslib-angular/components/verify-master-password.component"; +import { A11yInvalidDirective } from "jslib-angular/directives/a11y-invalid.directive"; import { A11yTitleDirective } from "jslib-angular/directives/a11y-title.directive"; import { ApiActionDirective } from "jslib-angular/directives/api-action.directive"; import { AutofocusDirective } from "jslib-angular/directives/autofocus.directive"; import { BlurClickDirective } from "jslib-angular/directives/blur-click.directive"; import { BoxRowDirective } from "jslib-angular/directives/box-row.directive"; import { FallbackSrcDirective } from "jslib-angular/directives/fallback-src.directive"; +import { InputStripSpacesDirective } from "jslib-angular/directives/input-strip-spaces.directive"; import { InputVerbatimDirective } from "jslib-angular/directives/input-verbatim.directive"; import { SelectCopyDirective } from "jslib-angular/directives/select-copy.directive"; import { StopClickDirective } from "jslib-angular/directives/stop-click.directive"; @@ -293,17 +295,18 @@ registerLocaleData(localeZhTw, "zh-TW"); ], declarations: [ A11yTitleDirective, + A11yInvalidDirective, AcceptEmergencyComponent, - AccessComponent, AcceptOrganizationComponent, + AccessComponent, AccountComponent, - SetPasswordComponent, AddCreditComponent, AddEditComponent, AddEditCustomFieldsComponent, + AddEditCustomFieldsComponent, AdjustPaymentComponent, - AdjustSubscription, AdjustStorageComponent, + AdjustSubscription, ApiActionDirective, ApiKeyComponent, AttachmentsComponent, @@ -329,6 +332,7 @@ registerLocaleData(localeZhTw, "zh-TW"); DeauthorizeSessionsComponent, DeleteAccountComponent, DeleteOrganizationComponent, + DisableSendPolicyComponent, DomainRulesComponent, DownloadLicenseComponent, EmergencyAccessAddEditComponent, @@ -352,22 +356,26 @@ registerLocaleData(localeZhTw, "zh-TW"); IconComponent, ImportComponent, InactiveTwoFactorReportComponent, + InputStripSpacesDirective, InputVerbatimDirective, LinkSsoComponent, LockComponent, LoginComponent, + MasterPasswordPolicyComponent, NavbarComponent, NestedCheckboxComponent, OptionsComponent, OrgAccountComponent, OrgAddEditComponent, OrganizationBillingComponent, + OrganizationLayoutComponent, OrganizationPlansComponent, + OrganizationsComponent, OrganizationSubscriptionComponent, OrgAttachmentsComponent, - OrgBulkStatusComponent, OrgBulkConfirmComponent, OrgBulkRemoveComponent, + OrgBulkStatusComponent, OrgCiphersComponent, OrgCollectionAddEditComponent, OrgCollectionsComponent, @@ -376,49 +384,56 @@ registerLocaleData(localeZhTw, "zh-TW"); OrgEventsComponent, OrgExportComponent, OrgExposedPasswordsReportComponent, - OrgImportComponent, - OrgInactiveTwoFactorReportComponent, OrgGroupAddEditComponent, OrgGroupingsComponent, OrgGroupsComponent, + OrgImportComponent, + OrgInactiveTwoFactorReportComponent, OrgManageCollectionsComponent, OrgManageComponent, OrgPeopleComponent, - OrgPolicyEditComponent, OrgPoliciesComponent, + OrgPolicyEditComponent, OrgResetPasswordComponent, OrgReusedPasswordsReportComponent, OrgSettingComponent, OrgToolsComponent, OrgTwoFactorSetupComponent, + OrgUnsecuredWebsitesReportComponent, OrgUserAddEditComponent, OrgUserConfirmComponent, OrgUserGroupsComponent, - OrganizationsComponent, - OrganizationLayoutComponent, - OrgUnsecuredWebsitesReportComponent, OrgVaultComponent, OrgWeakPasswordsReportComponent, PasswordGeneratorComponent, PasswordGeneratorHistoryComponent, - PasswordStrengthComponent, + PasswordGeneratorPolicyComponent, PasswordRepromptComponent, + PasswordStrengthComponent, PaymentComponent, + PersonalOwnershipPolicyComponent, PremiumComponent, ProfileComponent, + ProvidersComponent, PurgeVaultComponent, RecoverDeleteComponent, RecoverTwoFactorComponent, RegisterComponent, + RemovePasswordComponent, + RequireSsoPolicyComponent, + ResetPasswordPolicyComponent, ReusedPasswordsReportComponent, SearchCiphersPipe, SearchPipe, SelectCopyDirective, SendAddEditComponent, - SendEffluxDatesComponent, SendComponent, + SendEffluxDatesComponent, + SendOptionsPolicyComponent, + SetPasswordComponent, SettingsComponent, ShareComponent, + SingleOrgPolicyComponent, SponsoredFamiliesComponent, SponsoringOrgRowComponent, SsoComponent, @@ -427,6 +442,7 @@ registerLocaleData(localeZhTw, "zh-TW"); TaxInfoComponent, ToolsComponent, TrueFalseValueDirective, + TwoFactorAuthenticationPolicyComponent, TwoFactorAuthenticatorComponent, TwoFactorComponent, TwoFactorDuoComponent, @@ -444,41 +460,31 @@ registerLocaleData(localeZhTw, "zh-TW"); UpdatePasswordComponent, UserBillingComponent, UserLayoutComponent, - UserSubscriptionComponent, UserNamePipe, + UserSubscriptionComponent, VaultComponent, + VaultTimeoutInputComponent, VerifyEmailComponent, VerifyEmailTokenComponent, + VerifyMasterPasswordComponent, VerifyRecoverDeleteComponent, WeakPasswordsReportComponent, - ProvidersComponent, - TwoFactorAuthenticationPolicyComponent, - MasterPasswordPolicyComponent, - SingleOrgPolicyComponent, - PasswordGeneratorPolicyComponent, - RequireSsoPolicyComponent, - PersonalOwnershipPolicyComponent, - DisableSendPolicyComponent, - SendOptionsPolicyComponent, - ResetPasswordPolicyComponent, - VaultTimeoutInputComponent, - AddEditCustomFieldsComponent, - VerifyMasterPasswordComponent, - RemovePasswordComponent, ], exports: [ A11yTitleDirective, + A11yInvalidDirective, + ApiActionDirective, AvatarComponent, CalloutComponent, - ApiActionDirective, + FooterComponent, + I18nPipe, + InputStripSpacesDirective, + NavbarComponent, + OrganizationPlansComponent, + SearchPipe, StopClickDirective, StopPropDirective, - I18nPipe, - SearchPipe, UserNamePipe, - NavbarComponent, - FooterComponent, - OrganizationPlansComponent, ], providers: [DatePipe, SearchPipe, UserNamePipe], bootstrap: [], diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 4bf499b1e1..147f78340b 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -4414,25 +4414,25 @@ "message": "OIDC Redirect Behavior" }, "getClaimsFromUserInfoEndpoint": { - "message": "Get Claims From User Info Endpoint" + "message": "Get claims from user info endpoint" }, "additionalScopes": { - "message": "Additional/Custom Scopes (comma delimited)" + "message": "Custom Scopes" }, "additionalUserIdClaimTypes": { - "message": "Additional/Custom User ID Claim Types (comma delimited)" + "message": "Custom User ID Claim Types" }, "additionalEmailClaimTypes": { - "message": "Additional/Custom Email Claim Types (comma delimited)" + "message": "Email Claim Types" }, "additionalNameClaimTypes": { - "message": "Additional/Custom Name Claim Types (comma delimited)" + "message": "Custom Name Claim Types" }, "acrValues": { - "message": "Requested Authentication Context Class Reference values (acr_values)" + "message": "Requested Authentication Context Class Reference values" }, "expectedReturnAcrValue": { - "message": "Expected \"acr\" Claim Value In Response (acr validation)" + "message": "Expected \"acr\" Claim Value In Response" }, "spEntityId": { "message": "SP Entity ID" @@ -4456,10 +4456,10 @@ "message": "Minimum Incoming Signing Algorithm" }, "spWantAssertionsSigned": { - "message": "Want Assertions Signed" + "message": "Expect signed assertions" }, "spValidateCertificates": { - "message": "Validate Certificates" + "message": "Validate certificates" }, "idpEntityId": { "message": "Entity ID" @@ -4473,9 +4473,6 @@ "idpSingleLogoutServiceUrl": { "message": "Single Log Out Service URL" }, - "idpArtifactResolutionServiceUrl": { - "message": "Artifact Resolution Service URL" - }, "idpX509PublicCert": { "message": "X509 Public Certificate" }, @@ -4483,13 +4480,13 @@ "message": "Outbound Signing Algorithm" }, "idpAllowUnsolicitedAuthnResponse": { - "message": "Allow Unsolicited Authentication Response" + "message": "Allow unsolicited authentication response" }, - "idpDisableOutboundLogoutRequests": { - "message": "Disable Outbound Logout Requests" + "idpAllowOutboundLogoutRequests": { + "message": "Allow outbound logout requests" }, - "idpWantAuthnRequestsSigned": { - "message": "Want Authentication Requests Signed" + "idpSignAuthenticationRequests": { + "message": "Sign authentication requests" }, "ssoSettingsSaved": { "message": "Single Sign-On configuration was saved." @@ -4740,6 +4737,42 @@ "freeWithSponsorship": { "message": "FREE with sponsorship" }, + "formErrorSummaryPlural": { + "message": "$COUNT$ fields above need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, + "formErrorSummarySingle": { + "message": "1 field above needs your attention." + }, + "fieldRequiredError": { + "message": "$FIELDNAME$ is required.", + "placeholders": { + "fieldname": { + "content": "$1", + "example": "Full name" + } + } + }, + "required": { + "message": "required" + }, + "idpSingleSignOnServiceUrlRequired": { + "message": "Required if Entity ID is not a URL." + }, + "openIdOptionalCustomizations": { + "message": "Optional Customizations" + }, + "openIdAuthorityRequired": { + "message": "Required if Authority is not valid." + }, + "separateMultipleWithComma": { + "message": "Separate multiple with a comma." + }, "sessionTimeout": { "message": "Your session has timed out. Please go back and try logging in again." }, diff --git a/src/scss/forms.scss b/src/scss/forms.scss index 5406fb6f67..acba154f30 100644 --- a/src/scss/forms.scss +++ b/src/scss/forms.scss @@ -210,6 +210,42 @@ input[type="checkbox"] { } } +.section-header { + h3, + .btn.btn-link { + @include themify($themes) { + color: themed("headingColor"); + } + } + + h3 { + font-weight: normal; + text-transform: uppercase; + } +} + +.error-summary { + margin-top: 1rem; +} + +.error-inline { + @include themify($themes) { + color: themed("danger"); + } +} + +// Theming for invalid form elements in the SSO Config Form only +// Will be deprecated by component-level styling in the Component Library +app-org-manage-sso form { + .form-control.ng-invalid, + app-input-text.ng-invalid .form-control, + app-select.ng-invalid .form-control { + @include themify($themes) { + border-color: themed("danger"); + } + } +} + // Browser specific icons overlayed on input fields. e.g. caps lock indicator on password field ::-webkit-calendar-picker-indicator, input::-webkit-caps-lock-indicator,