bitwarden-estensione-browser/bitwarden_license/bit-web/src/app/organizations/manage/sso.component.ts

325 lines
11 KiB
TypeScript

import { Component, OnInit } from "@angular/core";
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { SelectOptions } from "@bitwarden/angular/interfaces/selectOptions";
import { dirtyRequired } from "@bitwarden/angular/validators/dirty.validator";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import {
OpenIdConnectRedirectBehavior,
Saml2BindingType,
Saml2NameIdFormat,
Saml2SigningBehavior,
SsoType,
} from "@bitwarden/common/enums/ssoEnums";
import { Utils } from "@bitwarden/common/misc/utils";
import { SsoConfigApi } from "@bitwarden/common/models/api/ssoConfigApi";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { OrganizationSsoRequest } from "@bitwarden/common/models/request/organization/organizationSsoRequest";
import { OrganizationSsoResponse } from "@bitwarden/common/models/response/organization/organizationSsoResponse";
import { SsoConfigView } from "@bitwarden/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",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class SsoComponent implements OnInit {
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<OrganizationSsoResponse>;
callbackPath: string;
signedOutCallbackPath: string;
spEntityId: string;
spMetadataUrl: string;
spAcsUrl: string;
enabled = this.formBuilder.control(false);
openIdForm = this.formBuilder.group(
{
authority: ["", dirtyRequired],
clientId: ["", dirtyRequired],
clientSecret: ["", dirtyRequired],
metadataAddress: [],
redirectBehavior: [OpenIdConnectRedirectBehavior.RedirectGet, dirtyRequired],
getClaimsFromUserInfoEndpoint: [],
additionalScopes: [],
additionalUserIdClaimTypes: [],
additionalEmailClaimTypes: [],
additionalNameClaimTypes: [],
acrValues: [],
expectedReturnAcrValue: [],
},
{
updateOn: "blur",
}
);
samlForm = this.formBuilder.group(
{
spNameIdFormat: [Saml2NameIdFormat.NotConfigured],
spOutboundSigningAlgorithm: [defaultSigningAlgorithm],
spSigningBehavior: [Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned],
spMinIncomingSigningAlgorithm: [defaultSigningAlgorithm],
spWantAssertionsSigned: [],
spValidateCertificates: [],
idpEntityId: ["", dirtyRequired],
idpBindingType: [Saml2BindingType.HttpRedirect],
idpSingleSignOnServiceUrl: [],
idpSingleLogoutServiceUrl: [],
idpX509PublicCert: ["", dirtyRequired],
idpOutboundSigningAlgorithm: [defaultSigningAlgorithm],
idpAllowUnsolicitedAuthnResponse: [],
idpAllowOutboundLogoutRequests: [true],
idpWantAuthnRequestsSigned: [],
},
{
updateOn: "blur",
}
);
ssoConfigForm = this.formBuilder.group({
configType: [SsoType.None],
keyConnectorEnabled: [false],
keyConnectorUrl: [""],
openId: this.openIdForm,
saml: this.samlForm,
});
constructor(
private formBuilder: UntypedFormBuilder,
private route: ActivatedRoute,
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction
) {}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
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")
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
.valueChanges.subscribe(() =>
this.samlForm.get("idpX509PublicCert").updateValueAndValidity()
);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
await this.load();
});
}
async load() {
this.organization = await this.organizationService.get(this.organizationId);
const ssoSettings = await this.organizationApiService.getSso(this.organizationId);
this.populateForm(ssoSettings);
this.callbackPath = ssoSettings.urls.callbackPath;
this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath;
this.spEntityId = ssoSettings.urls.spEntityId;
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
this.spAcsUrl = ssoSettings.urls.spAcsUrl;
this.loading = false;
}
async submit() {
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.organizationApiService.updateSso(this.organizationId, request);
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;
}
async validateKeyConnectorUrl() {
if (this.haveTestedKeyConnector) {
return;
}
this.keyConnectorUrl.markAsPending();
try {
await this.apiService.getKeyConnectorAlive(this.keyConnectorUrl.value);
this.keyConnectorUrl.updateValueAndValidity();
} catch {
this.keyConnectorUrl.setErrors({
invalidUrl: true,
});
}
this.haveTestedKeyConnector = true;
}
toggleOpenIdCustomizations() {
this.showOpenIdCustomizations = !this.showOpenIdCustomizations;
}
getErrorCount(form: UntypedFormGroup): number {
return Object.values(form.controls).reduce((acc: number, control: AbstractControl) => {
if (control instanceof UntypedFormGroup) {
return acc + this.getErrorCount(control);
}
if (control.errors == null) {
return acc;
}
return acc + Object.keys(control.errors).length;
}, 0);
}
get enableTestKeyConnector() {
return (
this.ssoConfigForm.get("keyConnectorEnabled").value &&
!Utils.isNullOrWhitespace(this.keyConnectorUrl?.value)
);
}
get keyConnectorUrl() {
return this.ssoConfigForm.get("keyConnectorUrl");
}
get samlSigningAlgorithmOptions(): SelectOptions[] {
return this.samlSigningAlgorithms.map((algorithm) => ({ name: algorithm, value: algorithm }));
}
private validateForm(form: UntypedFormGroup) {
Object.values(form.controls).forEach((control: AbstractControl) => {
if (control.disabled) {
return;
}
if (control instanceof UntypedFormGroup) {
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);
}
}