diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 4f230dd9883..a7cee53e08b 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -31,6 +31,9 @@ "useSingleSignOn": { "message": "Use single sign-on" }, + "yourOrganizationRequiresSingleSignOn": { + "message": "Your organization requires single sign-on." + }, "welcomeBack": { "message": "Welcome back" }, diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html b/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html index 77801edc8fe..625c92e38c5 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html @@ -48,11 +48,13 @@
- + @if (!viewOnly) { + + } diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso-manage.component.ts similarity index 85% rename from bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts rename to bitwarden_license/bit-web/src/app/auth/sso/sso-manage.component.ts index 1c25283ea4f..c7479df7784 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso-manage.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { AbstractControl, @@ -55,11 +53,11 @@ const defaultSigningAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha2 // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ - selector: "app-org-manage-sso", - templateUrl: "sso.component.html", + selector: "auth-sso-manage", + templateUrl: "sso-manage.component.html", standalone: false, }) -export class SsoComponent implements OnInit, OnDestroy { +export class SsoManageComponent implements OnInit, OnDestroy { readonly ssoType = SsoType; readonly memberDecryptionType = MemberDecryptionType; @@ -117,31 +115,31 @@ export class SsoComponent implements OnInit, OnDestroy { isInitializing = true; // concerned with UI/UX (i.e. when to show loading spinner vs form) isFormValidatingOrPopulating = true; // tracks when form fields are being validated/populated during load() or submit() - configuredKeyConnectorUrlFromServer: string | null; + configuredKeyConnectorUrlFromServer: string | null = null; memberDecryptionTypeValueChangesSubscription: Subscription | null = null; haveTestedKeyConnector = false; - organizationId: string; - organization: Organization; + organizationId: string | undefined = undefined; + organization: Organization | undefined = undefined; - callbackPath: string; - signedOutCallbackPath: string; - spEntityId: string; - spEntityIdStatic: string; - spMetadataUrl: string; - spAcsUrl: string; + callbackPath: string | undefined = undefined; + signedOutCallbackPath: string | undefined = undefined; + spEntityId: string | undefined = undefined; + spEntityIdStatic: string | undefined = undefined; + spMetadataUrl: string | undefined = undefined; + spAcsUrl: string | undefined = undefined; showClientSecret = false; protected openIdForm = this.formBuilder.group>( { - authority: new FormControl("", Validators.required), - clientId: new FormControl("", Validators.required), - clientSecret: new FormControl("", Validators.required), + authority: new FormControl("", { nonNullable: true, validators: Validators.required }), + clientId: new FormControl("", { nonNullable: true, validators: Validators.required }), + clientSecret: new FormControl("", { nonNullable: true, validators: Validators.required }), metadataAddress: new FormControl(), - redirectBehavior: new FormControl( - OpenIdConnectRedirectBehavior.RedirectGet, - Validators.required, - ), + redirectBehavior: new FormControl(OpenIdConnectRedirectBehavior.RedirectGet, { + nonNullable: true, + validators: Validators.required, + }), getClaimsFromUserInfoEndpoint: new FormControl(), additionalScopes: new FormControl(), additionalUserIdClaimTypes: new FormControl(), @@ -157,22 +155,32 @@ export class SsoComponent implements OnInit, OnDestroy { protected samlForm = this.formBuilder.group>( { - spUniqueEntityId: new FormControl(true, { updateOn: "change" }), - spNameIdFormat: new FormControl(Saml2NameIdFormat.NotConfigured), - spOutboundSigningAlgorithm: new FormControl(defaultSigningAlgorithm), - spSigningBehavior: new FormControl(Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned), - spMinIncomingSigningAlgorithm: new FormControl(defaultSigningAlgorithm), + spUniqueEntityId: new FormControl(true, { nonNullable: true, updateOn: "change" }), + spNameIdFormat: new FormControl(Saml2NameIdFormat.NotConfigured, { nonNullable: true }), + spOutboundSigningAlgorithm: new FormControl(defaultSigningAlgorithm, { nonNullable: true }), + spSigningBehavior: new FormControl(Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned, { + nonNullable: true, + }), + spMinIncomingSigningAlgorithm: new FormControl(defaultSigningAlgorithm, { + nonNullable: true, + }), spWantAssertionsSigned: new FormControl(), spValidateCertificates: new FormControl(), - idpEntityId: new FormControl("", Validators.required), - idpBindingType: new FormControl(Saml2BindingType.HttpRedirect), - idpSingleSignOnServiceUrl: new FormControl("", Validators.required), + idpEntityId: new FormControl("", { nonNullable: true, validators: Validators.required }), + idpBindingType: new FormControl(Saml2BindingType.HttpRedirect, { nonNullable: true }), + idpSingleSignOnServiceUrl: new FormControl("", { + nonNullable: true, + validators: Validators.required, + }), idpSingleLogoutServiceUrl: new FormControl(), - idpX509PublicCert: new FormControl("", Validators.required), - idpOutboundSigningAlgorithm: new FormControl(defaultSigningAlgorithm), + idpX509PublicCert: new FormControl("", { + nonNullable: true, + validators: Validators.required, + }), + idpOutboundSigningAlgorithm: new FormControl(defaultSigningAlgorithm, { nonNullable: true }), idpAllowUnsolicitedAuthnResponse: new FormControl(), - idpAllowOutboundLogoutRequests: new FormControl(true), + idpAllowOutboundLogoutRequests: new FormControl(true, { nonNullable: true }), idpWantAuthnRequestsSigned: new FormControl(), }, { @@ -181,13 +189,16 @@ export class SsoComponent implements OnInit, OnDestroy { ); protected ssoConfigForm = this.formBuilder.group>({ - configType: new FormControl(SsoType.None), - memberDecryptionType: new FormControl(MemberDecryptionType.MasterPassword), - keyConnectorUrl: new FormControl(""), + configType: new FormControl(SsoType.None, { nonNullable: true }), + memberDecryptionType: new FormControl(MemberDecryptionType.MasterPassword, { + nonNullable: true, + }), + keyConnectorUrl: new FormControl("", { nonNullable: true }), openId: this.openIdForm, saml: this.samlForm, - enabled: new FormControl(false), + enabled: new FormControl(false, { nonNullable: true }), ssoIdentifier: new FormControl("", { + nonNullable: true, validators: [Validators.maxLength(50), Validators.required], }), }); @@ -235,7 +246,7 @@ export class SsoComponent implements OnInit, OnDestroy { this.ssoConfigForm .get("configType") - .valueChanges.pipe(takeUntil(this.destroy$)) + ?.valueChanges.pipe(takeUntil(this.destroy$)) .subscribe((newType: SsoType) => { if (newType === SsoType.OpenIdConnect) { this.openIdForm.enable(); @@ -251,8 +262,8 @@ export class SsoComponent implements OnInit, OnDestroy { this.samlForm .get("spSigningBehavior") - .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe(() => this.samlForm.get("idpX509PublicCert").updateValueAndValidity()); + ?.valueChanges.pipe(takeUntil(this.destroy$)) + .subscribe(() => this.samlForm.get("idpX509PublicCert")?.updateValueAndValidity()); this.route.params .pipe( @@ -286,6 +297,10 @@ export class SsoComponent implements OnInit, OnDestroy { this.memberDecryptionTypeValueChangesSubscription = null; try { + if (!this.organizationId) { + throw new Error("Load: Organization ID is not set"); + } + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.organization = await firstValueFrom( this.organizationService @@ -334,6 +349,11 @@ export class SsoComponent implements OnInit, OnDestroy { this.readOutErrors(); return; } + + if (!this.organizationId) { + throw new Error("Submit: Organization ID is not set"); + } + const request = new OrganizationSsoRequest(); request.enabled = this.enabledCtrl.value; // Return null instead of empty string to avoid duplicate id errors in database @@ -349,7 +369,6 @@ export class SsoComponent implements OnInit, OnDestroy { this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t("ssoSettingsSaved"), }); } finally { @@ -407,16 +426,16 @@ export class SsoComponent implements OnInit, OnDestroy { return; } - this.keyConnectorUrl.markAsPending(); + this.keyConnectorUrlFormCtrl.markAsPending(); try { - await this.apiService.getKeyConnectorAlive(this.keyConnectorUrl.value); - this.keyConnectorUrl.updateValueAndValidity(); + await this.apiService.getKeyConnectorAlive(this.keyConnectorUrlFormCtrl.value); + this.keyConnectorUrlFormCtrl.updateValueAndValidity(); } catch { - this.keyConnectorUrl.setErrors({ + this.keyConnectorUrlFormCtrl.setErrors({ invalidUrl: { message: this.i18nService.t("keyConnectorTestFail") }, }); - this.keyConnectorUrl.markAllAsTouched(); + this.keyConnectorUrlFormCtrl.markAllAsTouched(); } this.haveTestedKeyConnector = true; @@ -442,12 +461,12 @@ export class SsoComponent implements OnInit, OnDestroy { get enableTestKeyConnector() { return ( this.ssoConfigForm.value?.memberDecryptionType === MemberDecryptionType.KeyConnector && - !Utils.isNullOrWhitespace(this.keyConnectorUrl?.value) + !Utils.isNullOrWhitespace(this.keyConnectorUrlFormCtrl?.value) ); } - get keyConnectorUrl() { - return this.ssoConfigForm.get("keyConnectorUrl"); + get keyConnectorUrlFormCtrl() { + return this.ssoConfigForm.controls?.keyConnectorUrl as FormControl; } /** @@ -502,6 +521,11 @@ export class SsoComponent implements OnInit, OnDestroy { organizationSsoRequest: OrganizationSsoRequest, ): Promise { const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!this.organizationId) { + throw new Error("upsertOrganizationWithSsoChanges: Organization ID is not set"); + } + const currentOrganization = await firstValueFrom( this.organizationService .organizations$(userId) diff --git a/libs/auth/src/angular/login/login.component.html b/libs/auth/src/angular/login/login.component.html index 26e19f11147..4e1689b1054 100644 --- a/libs/auth/src/angular/login/login.component.html +++ b/libs/auth/src/angular/login/login.component.html @@ -44,6 +44,8 @@ block buttonType="primary" (click)="continuePressed()" + [bitTooltip]="ssoRequired ? ('yourOrganizationRequiresSingleSignOn' | i18n) : ''" + [addTooltipToDescribedby]="ssoRequired" [disabled]="ssoRequired" > {{ "continue" | i18n }} @@ -59,6 +61,8 @@ block buttonType="secondary" (click)="handleLoginWithPasskeyClick()" + [bitTooltip]="ssoRequired ? ('yourOrganizationRequiresSingleSignOn' | i18n) : ''" + [addTooltipToDescribedby]="ssoRequired" [disabled]="ssoRequired" > @@ -67,7 +71,13 @@ - diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 537a42700c8..54a2a3b732b 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -54,6 +54,7 @@ import { IconButtonModule, LinkModule, ToastService, + TooltipDirective, } from "@bitwarden/components"; import { LoginComponentService, PasswordPolicies } from "./login-component.service"; @@ -82,6 +83,7 @@ export enum LoginUiState { JslibModule, ReactiveFormsModule, RouterModule, + TooltipDirective, ], }) export class LoginComponent implements OnInit, OnDestroy { diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index f36a3fdddf5..643b5d69da7 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -42,6 +42,7 @@ export * from "./table"; export * from "./tabs"; export * from "./toast"; export * from "./toggle-group"; +export * from "./tooltip"; export * from "./typography"; export * from "./utils"; export * from "./stepper"; diff --git a/package-lock.json b/package-lock.json index d1858d4d508..f6e33174737 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "lowdb": "1.0.0", "lunr": "2.3.9", "multer": "2.0.2", - "ngx-toastr": "19.0.0", + "ngx-toastr": "19.1.0", "node-fetch": "2.6.12", "node-forge": "1.3.1", "oidc-client-ts": "2.4.1", @@ -31032,9 +31032,9 @@ } }, "node_modules/ngx-toastr": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-19.0.0.tgz", - "integrity": "sha512-6pTnktwwWD+kx342wuMOWB4+bkyX9221pAgGz3SHOJH0/MI9erLucS8PeeJDFwbUYyh75nQ6AzVtolgHxi52dQ==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-19.1.0.tgz", + "integrity": "sha512-Qa7Kg7QzGKNtp1v04hu3poPKKx8BGBD/Onkhm6CdH5F0vSMdq+BdR/f8DTpZnGFksW891tAFufpiWb9UZX+3vg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" diff --git a/package.json b/package.json index 32056a174b1..12df878b5cf 100644 --- a/package.json +++ b/package.json @@ -193,7 +193,7 @@ "lowdb": "1.0.0", "lunr": "2.3.9", "multer": "2.0.2", - "ngx-toastr": "19.0.0", + "ngx-toastr": "19.1.0", "node-fetch": "2.6.12", "node-forge": "1.3.1", "oidc-client-ts": "2.4.1",