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

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

325 lines
11 KiB
TypeScript
Raw Normal View History

import { Component, OnInit } from "@angular/core";
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
2022-06-14 17:10:53 +02:00
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";
2022-06-14 17:10:53 +02:00
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import {
OpenIdConnectRedirectBehavior,
Saml2BindingType,
Saml2NameIdFormat,
Saml2SigningBehavior,
SsoType,
2022-06-14 17:10:53 +02:00
} 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",
];
2021-12-17 15:57:11 +01:00
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>;
2021-12-17 15:57:11 +01:00
callbackPath: string;
signedOutCallbackPath: string;
spEntityId: string;
spMetadataUrl: string;
spAcsUrl: string;
2021-12-17 15:57:11 +01:00
2021-12-31 16:06:07 +01:00
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,
2021-12-17 15:57:11 +01:00
});
constructor(
private formBuilder: UntypedFormBuilder,
private route: ActivatedRoute,
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction
2021-12-17 15:57:11 +01:00
) {}
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();
});
2021-12-17 15:57:11 +01:00
}
[Account Switching] [Refactor] Implement new account centric services (#1220) * [chore] updated services.module to use account services * [refactor] sorted services provided by services.module * [chore] removed references to deleted jslib services * [chore] used activeAccount over storageService for account level storage items * [chore] resolved linter warnings * Refactor activeAccountService to stateService * [bug] Remove uneeded calls to state service on logout This was causing console erros on logout. Clearing of data is handled fully in dedicated services, clearing them in state afterwards is essentially a redundant call. * [bug] Add back null locked callback to VaultTimeoutService * Move call to get showUpdateKey * [bug] Ensure HtmlStorageService does not override StateService options and locations * [bug] Adjust theme logic to pull from the new storage locations * [bug] Correct theme not sticking on refresh * [bug] Add enableFullWidth to the account model * [bug] fix theme option empty when light is selected * [bug] init state on application start * [bug] Reinit state when coming back from a lock * [style] Fix lint complaints * [bug] Clean state on logout * [chore] Resolved merge issues * [bug] Correct default for enableGravitars * Bump angular to 12. * Remove angular.json * Bump rxjs * Fix build errors, remove file-loader with asset/resource * Use contenthash * Bump jslib * Bump ngx-toastr * [chore] resolve issues from merge * [chore] resolve issues from merge * [bug] Add missing bracket * Use newer import syntax * [bug] Correct service orge * [style] Fix lint complaints * [chore] update jslib * [review] Address code review * [review] Address code review * [review] Rename providerService to webProviderService Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com> Co-authored-by: Hinton <oscar@oscarhinton.com>
2021-12-14 17:10:26 +01:00
async load() {
this.organization = await this.organizationService.get(this.organizationId);
const ssoSettings = await this.organizationApiService.getSso(this.organizationId);
this.populateForm(ssoSettings);
[Account Switching] [Refactor] Implement new account centric services (#1220) * [chore] updated services.module to use account services * [refactor] sorted services provided by services.module * [chore] removed references to deleted jslib services * [chore] used activeAccount over storageService for account level storage items * [chore] resolved linter warnings * Refactor activeAccountService to stateService * [bug] Remove uneeded calls to state service on logout This was causing console erros on logout. Clearing of data is handled fully in dedicated services, clearing them in state afterwards is essentially a redundant call. * [bug] Add back null locked callback to VaultTimeoutService * Move call to get showUpdateKey * [bug] Ensure HtmlStorageService does not override StateService options and locations * [bug] Adjust theme logic to pull from the new storage locations * [bug] Correct theme not sticking on refresh * [bug] Add enableFullWidth to the account model * [bug] fix theme option empty when light is selected * [bug] init state on application start * [bug] Reinit state when coming back from a lock * [style] Fix lint complaints * [bug] Clean state on logout * [chore] Resolved merge issues * [bug] Correct default for enableGravitars * Bump angular to 12. * Remove angular.json * Bump rxjs * Fix build errors, remove file-loader with asset/resource * Use contenthash * Bump jslib * Bump ngx-toastr * [chore] resolve issues from merge * [chore] resolve issues from merge * [bug] Add missing bracket * Use newer import syntax * [bug] Correct service orge * [style] Fix lint complaints * [chore] update jslib * [review] Address code review * [review] Address code review * [review] Rename providerService to webProviderService Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com> Co-authored-by: Hinton <oscar@oscarhinton.com>
2021-12-14 17:10:26 +01:00
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;
2021-12-17 15:57:11 +01:00
}
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
2021-12-17 15:57:11 +01:00
}
this.formPromise = null;
2021-12-17 15:57:11 +01:00
}
async validateKeyConnectorUrl() {
if (this.haveTestedKeyConnector) {
return;
}
this.keyConnectorUrl.markAsPending();
2021-12-17 15:57:11 +01:00
try {
await this.apiService.getKeyConnectorAlive(this.keyConnectorUrl.value);
this.keyConnectorUrl.updateValueAndValidity();
} catch {
this.keyConnectorUrl.setErrors({
invalidUrl: true,
2021-12-17 15:57:11 +01:00
});
}
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() {
2021-12-17 15:57:11 +01:00
return (
this.ssoConfigForm.get("keyConnectorEnabled").value &&
!Utils.isNullOrWhitespace(this.keyConnectorUrl?.value)
2021-12-17 15:57:11 +01:00
);
}
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);
}
}