1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-24 04:04:24 +00:00

refactor(SsoComponentRename): [Auth/PM-26745] Rename SSO management component to SsoManageComponent (#16893)

* PM-26745 - SsoComponent renamed to SsoManageComponent

* PM-26745 - SsoManageComponent - attempt to make strict ts.

* PM-26745 - Make SSO manage meet strict TS requirements
This commit is contained in:
Jared Snider
2025-10-30 10:11:57 -04:00
committed by GitHub
parent dcf8c1d83b
commit 062f5f9bd7
4 changed files with 79 additions and 55 deletions

View File

@@ -8,7 +8,7 @@ import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-con
import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/admin-console/organizations/layouts/organization-layout.component";
import { deepLinkGuard } from "@bitwarden/web-vault/app/auth/guards/deep-link/deep-link.guard";
import { SsoComponent } from "../../auth/sso/sso.component";
import { SsoManageComponent } from "../../auth/sso/sso-manage.component";
import { DomainVerificationComponent } from "./manage/domain-verification/domain-verification.component";
import { ScimComponent } from "./manage/scim.component";
@@ -33,7 +33,7 @@ const routes: Routes = [
},
{
path: "sso",
component: SsoComponent,
component: SsoManageComponent,
canActivate: [organizationPermissionsGuard((org) => org.canManageSso)],
data: {
titleId: "singleSignOn",

View File

@@ -3,7 +3,7 @@ import { NgModule } from "@angular/core";
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module";
import { SsoComponent } from "../../auth/sso/sso.component";
import { SsoManageComponent } from "../../auth/sso/sso-manage.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";
@@ -13,7 +13,7 @@ import { OrganizationsRoutingModule } from "./organizations-routing.module";
@NgModule({
imports: [SharedModule, OrganizationsRoutingModule, HeaderModule],
declarations: [
SsoComponent,
SsoManageComponent,
ScimComponent,
DomainVerificationComponent,
DomainAddEditDialogComponent,

View File

@@ -133,15 +133,15 @@
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="keyConnectorUrl.pending"
*ngIf="keyConnectorUrlFormCtrl.pending"
></i>
<span *ngIf="!keyConnectorUrl.pending">
<span *ngIf="!keyConnectorUrlFormCtrl.pending">
{{ "keyConnectorTest" | i18n }}
</span>
</button>
<bit-hint
aria-live="polite"
*ngIf="haveTestedKeyConnector && !keyConnectorUrl.hasError('invalidUrl')"
*ngIf="haveTestedKeyConnector && !keyConnectorUrlFormCtrl.hasError('invalidUrl')"
>
<small class="tw-text-success-600">
<i class="bwi bwi-check-circle" aria-hidden="true"></i>

View File

@@ -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<ControlsOf<SsoConfigView["openId"]>>(
{
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<ControlsOf<SsoConfigView["saml"]>>(
{
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<ControlsOf<SsoConfigView>>({
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<string>;
}
/**
@@ -502,6 +521,11 @@ export class SsoComponent implements OnInit, OnDestroy {
organizationSsoRequest: OrganizationSsoRequest,
): Promise<void> {
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)