1
0
mirror of https://github.com/bitwarden/web synced 2025-12-06 00:03:28 +00:00

Improve SSO Config validation (#1332)

* Break form controls up into reusable components

* Add proper form styling, validation, inline error messages, etc

* Move control options into class instead of template

* Add accessibility
This commit is contained in:
Thomas Rittson
2022-03-03 20:08:41 +10:00
committed by GitHub
parent cf9a90d10e
commit 06e1af6d48
16 changed files with 988 additions and 538 deletions

View File

@@ -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
}

View File

@@ -0,0 +1,16 @@
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[attr.id]="controlId"
[attr.aria-describedby]="describedById"
[formControl]="internalControl"
(blur)="onBlurInternal()"
/>
<label class="form-check-label" [attr.for]="controlId">{{ label }}</label>
</div>
<small *ngIf="showDescribedBy" [attr.id]="describedById" class="form-text text-muted">{{
helperText
}}</small>
</div>

View File

@@ -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 {}

View File

@@ -0,0 +1,26 @@
<div class="form-group">
<label>{{ label }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="controlValue" />
<div class="input-group-append" *ngIf="showLaunch">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'launch' | i18n }}"
(click)="launchUri(controlValue)"
>
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
</button>
</div>
<div class="input-group-append" *ngIf="showCopy">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(controlValue)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>

View File

@@ -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);
}
}

View File

@@ -0,0 +1,33 @@
<div class="form-group">
<label [attr.for]="controlId">
{{ label }}
<small *ngIf="isRequired" class="text-muted form-text d-inline"
>({{ "required" | i18n }})</small
>
</label>
<input
[formControl]="internalControl"
class="form-control"
[attr.id]="controlId"
[attr.aria-describedby]="describedById"
[attr.aria-invalid]="controlDir.control.invalid"
(blur)="onBlurInternal()"
/>
<div *ngIf="showDescribedBy" [attr.id]="describedById">
<small
*ngIf="helperText != null && !controlDir.control.hasError(helperTextSameAsError)"
class="form-text text-muted"
>
{{ helperText }}
</small>
<small class="error-inline" *ngIf="controlDir.control.hasError('required')" role="alert">
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{
controlDir.control.hasError(helperTextSameAsError)
? helperText
: ("fieldRequiredError" | i18n: label)
}}
</small>
</div>
</div>

View File

@@ -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, "");
}
}

View File

@@ -0,0 +1,19 @@
<div class="form-group">
<label [attr.for]="controlId">
{{ label }}
<small *ngIf="isRequired" class="text-muted form-text d-inline"
>({{ "required" | i18n }})</small
>
</label>
<select
class="form-control"
[attr.id]="controlId"
[attr.aria-invalid]="controlDir.control.invalid"
[formControl]="internalControl"
(blur)="onBlurInternal()"
>
<option *ngFor="let o of selectOptions" [ngValue]="o.value" disabled="{{ o.disabled }}">
{{ o.name }}
</option>
</select>
</div>

View File

@@ -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[];
}

View File

@@ -14,10 +14,9 @@
<form <form
#form #form
(ngSubmit)="submit()" (ngSubmit)="submit()"
[formGroup]="data" [formGroup]="ssoConfigForm"
[appApiAction]="formPromise" [appApiAction]="formPromise"
*ngIf="!loading" *ngIf="!loading"
ngNativeValidate
> >
<p> <p>
{{ "ssoPolicyHelpStart" | i18n }} {{ "ssoPolicyHelpStart" | i18n }}
@@ -27,451 +26,407 @@
{{ "ssoPolicyHelpKeyConnector" | i18n }} {{ "ssoPolicyHelpKeyConnector" | i18n }}
</p> </p>
<div class="form-group"> <!-- Root form -->
<div class="form-check"> <ng-container>
<input <app-input-checkbox
class="form-check-input" controlId="enabled"
type="checkbox" [formControl]="enabled"
id="enabled" [label]="'allowSso' | i18n"
[formControl]="enabled" [helperText]="'allowSsoDesc' | i18n"
name="Enabled" ></app-input-checkbox>
/>
<label class="form-check-label" for="enabled">{{ "allowSso" | i18n }}</label>
</div>
<small class="form-text text-muted">{{ "allowSsoDesc" | i18n }}</small>
</div>
<div class="form-group">
<label>{{ "memberDecryptionOption" | i18n }}</label>
<div class="form-check form-check-block">
<input
class="form-check-input"
type="radio"
id="memberDecryptionPass"
[value]="false"
formControlName="keyConnectorEnabled"
/>
<label class="form-check-label" for="memberDecryptionPass">
{{ "masterPass" | i18n }}
<small>{{ "memberDecryptionPassDesc" | i18n }}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
id="memberDecryptionKey"
[value]="true"
formControlName="keyConnectorEnabled"
[attr.disabled]="!organization.useKeyConnector || null"
/>
<label class="form-check-label" for="memberDecryptionKey">
{{ "keyConnector" | i18n }}
<a
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/about-key-connector/"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
<small>{{ "memberDecryptionKeyConnectorDesc" | i18n }}</small>
</label>
</div>
</div>
<ng-container *ngIf="data.value.keyConnectorEnabled">
<app-callout type="warning" [useAlertRole]="true">
{{ "keyConnectorWarning" | i18n }}
</app-callout>
<div class="form-group"> <div class="form-group">
<label for="keyConnectorUrl">{{ "keyConnectorUrl" | i18n }}</label> <label>{{ "memberDecryptionOption" | i18n }}</label>
<div class="input-group"> <div class="form-check form-check-block">
<input <input
class="form-control" class="form-check-input"
formControlName="keyConnectorUrl" type="radio"
id="keyConnectorUrl" id="memberDecryptionPass"
required [value]="false"
formControlName="keyConnectorEnabled"
/> />
<div class="input-group-append"> <label class="form-check-label" for="memberDecryptionPass">
<button {{ "masterPass" | i18n }}
type="button" <small>{{ "memberDecryptionPassDesc" | i18n }}</small>
class="btn btn-outline-secondary" </label>
(click)="validateKeyConnectorUrl()" </div>
[disabled]="!enableTestKeyConnector" <div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
id="memberDecryptionKey"
[value]="true"
formControlName="keyConnectorEnabled"
[attr.disabled]="!organization.useKeyConnector || null"
/>
<label class="form-check-label" for="memberDecryptionKey">
{{ "keyConnector" | i18n }}
<a
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/about-key-connector/"
> >
<i <i class="bwi bwi-question-circle" aria-hidden="true"></i>
class="bwi bwi-spinner bwi-spin" </a>
title="{{ 'loading' | i18n }}" <small>{{ "memberDecryptionKeyConnectorDesc" | i18n }}</small>
aria-hidden="true" </label>
*ngIf="keyConnectorUrl.pending" </div>
></i> </div>
<span *ngIf="!keyConnectorUrl.pending">
{{ "keyConnectorTest" | i18n }} <!-- Key Connector -->
</span> <ng-container *ngIf="ssoConfigForm.get('keyConnectorEnabled').value">
</button> <app-callout type="warning" [useAlertRole]="true">
{{ "keyConnectorWarning" | i18n }}
</app-callout>
<div class="form-group">
<label for="keyConnectorUrl">
{{ "keyConnectorUrl" | i18n }}
<small class="text-muted form-text d-inline">({{ "required" | i18n }})</small>
</label>
<div class="input-group">
<input
class="form-control"
formControlName="keyConnectorUrl"
id="keyConnectorUrl"
aria-describedby="keyConnectorUrlDesc"
(change)="haveTestedKeyConnector = false"
appInputStripSpaces
appA11yInvalid
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
(click)="validateKeyConnectorUrl()"
[disabled]="!enableTestKeyConnector"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="keyConnectorUrl.pending"
></i>
<span *ngIf="!keyConnectorUrl.pending">
{{ "keyConnectorTest" | i18n }}
</span>
</button>
</div>
</div>
<div *ngIf="haveTestedKeyConnector" id="keyConnectorUrlDesc" aria-live="polite">
<small
class="error-inline"
*ngIf="keyConnectorUrl.hasError('invalidUrl'); else keyConnectorSuccess"
>
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{ "keyConnectorTestFail" | i18n }}
</small>
<ng-template #keyConnectorSuccess>
<small class="text-success">
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
{{ "keyConnectorTestSuccess" | i18n }}
</small>
</ng-template>
</div> </div>
</div> </div>
<ng-container *ngIf="keyConnectorUrl.pristine && !keyConnectorUrl.pending"> </ng-container>
<div class="text-danger" *ngIf="keyConnectorUrl.hasError('invalidUrl')" role="alert">
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i> <app-select
{{ "keyConnectorTestFail" | i18n }} controlId="type"
</div> [label]="'type' | i18n"
<div class="text-success" *ngIf="!keyConnectorUrl.hasError('invalidUrl')" role="alert"> [selectOptions]="ssoTypeOptions"
<i class="bwi bwi-check-circle" aria-hidden="true"></i> formControlName="configType"
{{ "keyConnectorTestSuccess" | i18n }} >
</div> </app-select>
</ng-container>
</div>
</ng-container> </ng-container>
<div class="form-group">
<label for="type">{{ "type" | i18n }}</label>
<select class="form-control" id="type" formControlName="configType">
<option [ngValue]="0" disabled>{{ "selectType" | i18n }}</option>
<option [ngValue]="1">OpenID Connect</option>
<option [ngValue]="2">SAML 2.0</option>
</select>
</div>
<!-- OIDC --> <!-- OIDC -->
<div *ngIf="data.value.configType == 1"> <div
*ngIf="ssoConfigForm.get('configType').value === ssoType.OpenIdConnect"
[formGroup]="openIdForm"
>
<div class="config-section"> <div class="config-section">
<h2>{{ "openIdConnectConfig" | i18n }}</h2> <h2 class="secondary-header">{{ "openIdConnectConfig" | i18n }}</h2>
<div class="form-group">
<label>{{ "callbackPath" | i18n }}</label> <app-input-text-readonly
<div class="input-group"> [label]="'callbackPath' | i18n"
<input class="form-control" readonly [value]="callbackPath" /> [controlValue]="callbackPath"
<div class="input-group-append"> ></app-input-text-readonly>
<button
type="button" <app-input-text-readonly
class="btn btn-outline-secondary" [label]="'signedOutCallbackPath' | i18n"
appA11yTitle="{{ 'copyValue' | i18n }}" [controlValue]="signedOutCallbackPath"
(click)="copy(callbackPath)" ></app-input-text-readonly>
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i> <app-input-text
</button> [label]="'authority' | i18n"
</div> controlId="authority"
</div> [stripSpaces]="true"
formControlName="authority"
></app-input-text>
<app-input-text
[label]="'clientId' | i18n"
controlId="clientId"
[stripSpaces]="true"
formControlName="clientId"
></app-input-text>
<app-input-text
[label]="'clientSecret' | i18n"
controlId="clientSecret"
[stripSpaces]="true"
formControlName="clientSecret"
></app-input-text>
<app-input-text
[label]="'metadataAddress' | i18n"
controlId="metadataAddress"
[stripSpaces]="true"
[helperText]="'openIdAuthorityRequired' | i18n"
formControlName="metadataAddress"
></app-input-text>
<app-select
controlId="redirectBehavior"
[label]="'oidcRedirectBehavior' | i18n"
[selectOptions]="connectRedirectOptions"
formControlName="redirectBehavior"
>
</app-select>
<app-input-checkbox
controlId="getClaimsFromUserInfoEndpoint"
formControlName="getClaimsFromUserInfoEndpoint"
[label]="'getClaimsFromUserInfoEndpoint' | i18n"
></app-input-checkbox>
<!-- Optional customizations -->
<div
class="section-header d-flex flex-row align-items-center mt-3 mb-3"
(click)="toggleOpenIdCustomizations()"
>
<h3 class="mb-0 mr-2" id="customizations-header">
{{ "openIdOptionalCustomizations" | i18n }}
</h3>
<button
class="mb-1 btn btn-link"
type="button"
appStopClick
role="button"
aria-controls="customizations"
[attr.aria-expanded]="showOpenIdCustomizations"
aria-labelledby="customizations-header"
>
<i
class="bwi"
aria-hidden="true"
[ngClass]="{
'bwi-angle-down': !showOpenIdCustomizations,
'bwi-chevron-up': showOpenIdCustomizations
}"
></i>
</button>
</div> </div>
<div class="form-group"> <div id="customizations" [hidden]="!showOpenIdCustomizations">
<label>{{ "signedOutCallbackPath" | i18n }}</label> <app-input-text
<div class="input-group"> [label]="'additionalScopes' | i18n"
<input class="form-control" readonly [value]="signedOutCallbackPath" /> controlId="additionalScopes"
<div class="input-group-append"> [helperText]="'separateMultipleWithComma' | i18n"
<button formControlName="additionalScopes"
type="button" ></app-input-text>
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}" <app-input-text
(click)="copy(signedOutCallbackPath)" [label]="'additionalUserIdClaimTypes' | i18n"
> controlId="additionalUserIdClaimTypes"
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i> [helperText]="'separateMultipleWithComma' | i18n"
</button>
</div>
</div>
</div>
<div class="form-group">
<label for="authority">{{ "authority" | i18n }}</label>
<input class="form-control" formControlName="authority" id="authority" />
</div>
<div class="form-group">
<label for="clientId">{{ "clientId" | i18n }}</label>
<input class="form-control" formControlName="clientId" id="clientId" />
</div>
<div class="form-group">
<label for="clientSecret">{{ "clientSecret" | i18n }}</label>
<input class="form-control" formControlName="clientSecret" id="clientSecret" />
</div>
<div class="form-group">
<label for="metadataAddress">{{ "metadataAddress" | i18n }}</label>
<input class="form-control" formControlName="metadataAddress" id="metadataAddress" />
</div>
<div class="form-group">
<label for="redirectBehavior">{{ "oidcRedirectBehavior" | i18n }}</label>
<select class="form-control" formControlName="redirectBehavior" id="redirectBehavior">
<option [ngValue]="0">Redirect GET</option>
<option [ngValue]="1">Form POST</option>
</select>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="getClaimsFromUserInfoEndpoint"
formControlName="getClaimsFromUserInfoEndpoint"
/>
<label class="form-check-label" for="getClaimsFromUserInfoEndpoint">
{{ "getClaimsFromUserInfoEndpoint" | i18n }}
</label>
</div>
</div>
<div class="form-group">
<label for="additionalScopes">{{ "additionalScopes" | i18n }}</label>
<input class="form-control" formControlName="additionalScopes" id="additionalScopes" />
</div>
<div class="form-group">
<label for="additionalUserIdClaimTypes">{{ "additionalUserIdClaimTypes" | i18n }}</label>
<input
class="form-control"
formControlName="additionalUserIdClaimTypes" formControlName="additionalUserIdClaimTypes"
id="additionalUserIdClaimTypes" ></app-input-text>
/>
</div> <app-input-text
<div class="form-group"> [label]="'additionalEmailClaimTypes' | i18n"
<label for="additionalEmailClaimTypes">{{ "additionalEmailClaimTypes" | i18n }}</label> controlId="additionalEmailClaimTypes"
<input [helperText]="'separateMultipleWithComma' | i18n"
class="form-control"
formControlName="additionalEmailClaimTypes" formControlName="additionalEmailClaimTypes"
id="additionalEmailClaimTypes" ></app-input-text>
/>
</div> <app-input-text
<div class="form-group"> [label]="'additionalNameClaimTypes' | i18n"
<label for="additionalNameClaimTypes">{{ "additionalNameClaimTypes" | i18n }}</label> controlId="additionalNameClaimTypes"
<input [helperText]="'separateMultipleWithComma' | i18n"
class="form-control"
formControlName="additionalNameClaimTypes" formControlName="additionalNameClaimTypes"
id="additionalNameClaimTypes" ></app-input-text>
/>
</div> <app-input-text
<div class="form-group"> [label]="'acrValues' | i18n"
<label for="acrValues">{{ "acrValues" | i18n }}</label> controlId="acrValues"
<input class="form-control" formControlName="acrValues" id="acrValues" /> helperText="acr_values"
</div> formControlName="acrValues"
<div class="form-group"> ></app-input-text>
<label for="expectedReturnAcrValue">{{ "expectedReturnAcrValue" | i18n }}</label>
<input <app-input-text
class="form-control" [label]="'expectedReturnAcrValue' | i18n"
controlId="expectedReturnAcrValue"
helperText="acr_validation"
formControlName="expectedReturnAcrValue" formControlName="expectedReturnAcrValue"
id="expectedReturnAcrValue" ></app-input-text>
/>
</div> </div>
</div> </div>
</div> </div>
<div *ngIf="data.value.configType == 2"> <!-- SAML2 SP -->
<div *ngIf="ssoConfigForm.get('configType').value === ssoType.Saml2" [formGroup]="samlForm">
<!-- SAML2 SP --> <!-- SAML2 SP -->
<div class="config-section"> <div class="config-section">
<h2>{{ "samlSpConfig" | i18n }}</h2> <h2 class="secondary-header">{{ "samlSpConfig" | i18n }}</h2>
<div class="form-group">
<label>{{ "spEntityId" | i18n }}</label> <app-input-text-readonly
<div class="input-group"> [label]="'spEntityId' | i18n"
<input class="form-control" readonly [value]="spEntityId" /> [controlValue]="spEntityId"
<div class="input-group-append"> ></app-input-text-readonly>
<button
type="button" <app-input-text-readonly
class="btn btn-outline-secondary" [label]="'spMetadataUrl' | i18n"
appA11yTitle="{{ 'copyValue' | i18n }}" [controlValue]="spMetadataUrl"
(click)="copy(spEntityId)" [showLaunch]="true"
> ></app-input-text-readonly>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button> <app-input-text-readonly
</div> [label]="'spAcsUrl' | i18n"
</div> [controlValue]="spAcsUrl"
</div> ></app-input-text-readonly>
<div class="form-group">
<label>{{ "spMetadataUrl" | i18n }}</label> <app-select
<div class="input-group"> controlId="spNameIdFormat"
<input class="form-control" readonly [value]="spMetadataUrl" /> [label]="'spNameIdFormat' | i18n"
<div class="input-group-append"> [selectOptions]="saml2NameIdFormatOptions"
<button formControlName="spNameIdFormat"
type="button" >
class="btn btn-outline-secondary" </app-select>
appA11yTitle="{{ 'launch' | i18n }}"
(click)="launchUri(spMetadataUrl)" <app-select
> controlId="spOutboundSigningAlgorithm"
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i> [label]="'spOutboundSigningAlgorithm' | i18n"
</button> [selectOptions]="samlSigningAlgorithmOptions"
<button formControlName="spOutboundSigningAlgorithm"
type="button" >
class="btn btn-outline-secondary" </app-select>
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(spMetadataUrl)" <app-select
> controlId="spSigningBehavior"
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i> [label]="'spSigningBehavior' | i18n"
</button> [selectOptions]="saml2SigningBehaviourOptions"
</div> formControlName="spSigningBehavior"
</div> >
</div> </app-select>
<div class="form-group">
<label>{{ "spAcsUrl" | i18n }}</label> <app-select
<div class="input-group"> controlId="spMinIncomingSigningAlgorithm"
<input class="form-control" readonly [value]="spAcsUrl" /> [label]="'spMinIncomingSigningAlgorithm' | i18n"
<div class="input-group-append"> [selectOptions]="samlSigningAlgorithmOptions"
<button formControlName="spMinIncomingSigningAlgorithm"
type="button" >
class="btn btn-outline-secondary" </app-select>
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(spAcsUrl)" <app-input-checkbox
> controlId="spWantAssertionsSigned"
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i> formControlName="spWantAssertionsSigned"
</button> [label]="'spWantAssertionsSigned' | i18n"
</div> ></app-input-checkbox>
</div>
</div> <app-input-checkbox
<div class="form-group"> controlId="spValidateCertificates"
<label for="spNameIdFormat">{{ "spNameIdFormat" | i18n }}</label> formControlName="spValidateCertificates"
<select class="form-control" formControlName="spNameIdFormat" id="spNameIdFormat"> [label]="'spValidateCertificates' | i18n"
<option [ngValue]="0">Not Configured</option> ></app-input-checkbox>
<option [ngValue]="1">Unspecified</option>
<option [ngValue]="2">Email Address</option>
<option [ngValue]="3">X.509 Subject Name</option>
<option [ngValue]="4">Windows Domain Qualified Name</option>
<option [ngValue]="5">Kerberos Principal Name</option>
<option [ngValue]="6">Entity Identifier</option>
<option [ngValue]="7">Persistent</option>
<option [ngValue]="8">Transient</option>
</select>
</div>
<div class="form-group">
<label for="spOutboundSigningAlgorithm">{{ "spOutboundSigningAlgorithm" | i18n }}</label>
<select
class="form-control"
formControlName="spOutboundSigningAlgorithm"
id="spOutboundSigningAlgorithm"
>
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option>
</select>
</div>
<div class="form-group">
<label for="spSigningBehavior">{{ "spSigningBehavior" | i18n }}</label>
<select class="form-control" formControlName="spSigningBehavior" id="spSigningBehavior">
<option [ngValue]="0">If IdP Wants Authn Requests Signed</option>
<option [ngValue]="1">Always</option>
<option [ngValue]="3">Never</option>
</select>
</div>
<div class="form-group">
<label for="spMinIncomingSigningAlgorithm">{{
"spMinIncomingSigningAlgorithm" | i18n
}}</label>
<select
class="form-control"
formControlName="spMinIncomingSigningAlgorithm"
id="spMinIncomingSigningAlgorithm"
>
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option>
</select>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="spWantAssertionsSigned"
formControlName="spWantAssertionsSigned"
/>
<label class="form-check-label" for="spWantAssertionsSigned">
{{ "spWantAssertionsSigned" | i18n }}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="spValidateCertificates"
formControlName="spValidateCertificates"
/>
<label class="form-check-label" for="spValidateCertificates">
{{ "spValidateCertificates" | i18n }}
</label>
</div>
</div>
</div> </div>
<!-- SAML2 IDP --> <!-- SAML2 IDP -->
<div class="config-section"> <div class="config-section">
<h2>{{ "samlIdpConfig" | i18n }}</h2> <h2 class="secondary-header">{{ "samlIdpConfig" | i18n }}</h2>
<app-input-text
[label]="'idpEntityId' | i18n"
controlId="idpEntityId"
formControlName="idpEntityId"
></app-input-text>
<app-select
controlId="idpBindingType"
[label]="'idpBindingType' | i18n"
[selectOptions]="saml2BindingTypeOptions"
formControlName="idpBindingType"
>
</app-select>
<app-input-text
[label]="'idpSingleSignOnServiceUrl' | i18n"
controlId="idpSingleSignOnServiceUrl"
[helperText]="'idpSingleSignOnServiceUrlRequired' | i18n"
[stripSpaces]="true"
formControlName="idpSingleSignOnServiceUrl"
></app-input-text>
<app-input-text
[label]="'idpSingleLogoutServiceUrl' | i18n"
controlId="idpSingleLogoutServiceUrl"
[stripSpaces]="true"
formControlName="idpSingleLogoutServiceUrl"
></app-input-text>
<div class="form-group"> <div class="form-group">
<label for="idpEntityId">{{ "idpEntityId" | i18n }}</label> <label for="idpX509PublicCert">
<input class="form-control" formControlName="idpEntityId" id="idpEntityId" /> {{ "idpX509PublicCert" | i18n }}
</div> <small class="text-muted form-text d-inline">({{ "required" | i18n }})</small>
<div class="form-group"> </label>
<label for="idpBindingType">{{ "idpBindingType" | i18n }}</label>
<select class="form-control" formControlName="idpBindingType" id="idpBindingType">
<option [ngValue]="1">Redirect</option>
<option [ngValue]="2">HTTP POST</option>
</select>
</div>
<div class="form-group">
<label for="idpSingleSignOnServiceUrl">{{ "idpSingleSignOnServiceUrl" | i18n }}</label>
<input
class="form-control"
formControlName="idpSingleSignOnServiceUrl"
id="idpSingleSignOnServiceUrl"
/>
</div>
<div class="form-group">
<label for="idpSingleLogoutServiceUrl">{{ "idpSingleLogoutServiceUrl" | i18n }}</label>
<input
class="form-control"
formControlName="idpSingleLogoutServiceUrl"
id="idpSingleLogoutServiceUrl"
/>
</div>
<div class="form-group">
<label for="idpX509PublicCert">{{ "idpX509PublicCert" | i18n }}</label>
<textarea <textarea
formControlName="idpX509PublicCert" formControlName="idpX509PublicCert"
class="form-control form-control-sm text-monospace" class="form-control form-control-sm text-monospace"
rows="6" rows="6"
id="idpX509PublicCert" id="idpX509PublicCert"
appA11yInvalid
aria-describedby="idpX509PublicCertDesc"
></textarea> ></textarea>
</div> <small
<div class="form-group"> id="idpX509PublicCertDesc"
<label for="idpOutboundSigningAlgorithm">{{ "idpOutboundSigningAlgorithm" | i18n }}</label> class="error-inline"
<select role="alert"
class="form-control" *ngIf="samlForm.get('idpX509PublicCert').hasError('required')"
formControlName="idpOutboundSigningAlgorithm"
id="idpOutboundSigningAlgorithm"
> >
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option> <i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
</select> <span class="sr-only">{{ "error" | i18n }}:</span>
</div> {{ "fieldRequiredError" | i18n: ("idpX509PublicCert" | i18n) }}
<div class="form-group" [hidden]="true"> </small>
<!--TODO: Unhide once Unsolicited IdP Response is supported-->
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="idpAllowUnsolicitedAuthnResponse"
formControlName="idpAllowUnsolicitedAuthnResponse"
/>
<label class="form-check-label" for="idpAllowUnsolicitedAuthnResponse">
{{ "idpAllowUnsolicitedAuthnResponse" | i18n }}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="idpDisableOutboundLogoutRequests"
formControlName="idpDisableOutboundLogoutRequests"
/>
<label class="form-check-label" for="idpDisableOutboundLogoutRequests">
{{ "idpDisableOutboundLogoutRequests" | i18n }}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="idpWantAuthnRequestsSigned"
formControlName="idpWantAuthnRequestsSigned"
/>
<label class="form-check-label" for="idpWantAuthnRequestsSigned">
{{ "idpWantAuthnRequestsSigned" | i18n }}
</label>
</div>
</div> </div>
<app-select
controlId="idpOutboundSigningAlgorithm"
[label]="'idpOutboundSigningAlgorithm' | i18n"
[selectOptions]="samlSigningAlgorithmOptions"
formControlName="idpOutboundSigningAlgorithm"
>
</app-select>
<!--TODO: Uncomment once Unsolicited IdP Response is supported-->
<!-- <app-input-checkbox
controlId="idpAllowUnsolicitedAuthnResponse"
formControlName="idpAllowUnsolicitedAuthnResponse"
[label]="'idpAllowUnsolicitedAuthnResponse' | i18n"
></app-input-checkbox> -->
<app-input-checkbox
controlId="idpAllowOutboundLogoutRequests"
formControlName="idpAllowOutboundLogoutRequests"
[label]="'idpAllowOutboundLogoutRequests' | i18n"
></app-input-checkbox>
<app-input-checkbox
controlId="idpWantAuthnRequestsSigned"
formControlName="idpWantAuthnRequestsSigned"
[label]="'idpSignAuthenticationRequests' | i18n"
></app-input-checkbox>
</div> </div>
</div> </div>
@@ -479,4 +434,15 @@
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span> <span>{{ "save" | i18n }}</span>
</button> </button>
<div
id="errorSummary"
class="error-summary text-danger"
*ngIf="this.getErrorCount(ssoConfigForm) as errorCount"
>
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{
(errorCount === 1 ? "formErrorSummarySingle" : "formErrorSummaryPlural") | i18n: errorCount
}}
</div>
</form> </form>

View File

@@ -1,27 +1,82 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms"; import { AbstractControl, FormBuilder, FormGroup } from "@angular/forms";
import { ActivatedRoute } from "@angular/router"; 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 { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service"; import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.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 { Organization } from "jslib-common/models/domain/organization";
import { OrganizationSsoRequest } from "jslib-common/models/request/organization/organizationSsoRequest"; 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({ @Component({
selector: "app-org-manage-sso", selector: "app-org-manage-sso",
templateUrl: "sso.component.html", templateUrl: "sso.component.html",
}) })
export class SsoComponent implements OnInit { 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/2001/04/xmldsig-more#rsa-sha256",
"http://www.w3.org/2000/09/xmldsig#rsa-sha384", "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-sha512",
"http://www.w3.org/2000/09/xmldsig#rsa-sha1", "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; loading = true;
haveTestedKeyConnector = false;
organizationId: string; organizationId: string;
organization: Organization; organization: Organization;
formPromise: Promise<any>; formPromise: Promise<any>;
@@ -33,43 +88,57 @@ export class SsoComponent implements OnInit {
spAcsUrl: string; spAcsUrl: string;
enabled = this.formBuilder.control(false); enabled = this.formBuilder.control(false);
data = this.formBuilder.group({
configType: [],
keyConnectorEnabled: [], openIdForm = this.formBuilder.group(
keyConnectorUrl: [], {
authority: ["", dirtyRequired],
clientId: ["", dirtyRequired],
clientSecret: ["", dirtyRequired],
metadataAddress: [],
redirectBehavior: [OpenIdConnectRedirectBehavior.RedirectGet, dirtyRequired],
getClaimsFromUserInfoEndpoint: [],
additionalScopes: [],
additionalUserIdClaimTypes: [],
additionalEmailClaimTypes: [],
additionalNameClaimTypes: [],
acrValues: [],
expectedReturnAcrValue: [],
},
{
updateOn: "blur",
}
);
// OpenId samlForm = this.formBuilder.group(
authority: [], {
clientId: [], spNameIdFormat: [Saml2NameIdFormat.NotConfigured],
clientSecret: [], spOutboundSigningAlgorithm: [defaultSigningAlgorithm],
metadataAddress: [], spSigningBehavior: [Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned],
redirectBehavior: [], spMinIncomingSigningAlgorithm: [defaultSigningAlgorithm],
getClaimsFromUserInfoEndpoint: [], spWantAssertionsSigned: [],
additionalScopes: [], spValidateCertificates: [],
additionalUserIdClaimTypes: [],
additionalEmailClaimTypes: [],
additionalNameClaimTypes: [],
acrValues: [],
expectedReturnAcrValue: [],
// SAML idpEntityId: ["", dirtyRequired],
spNameIdFormat: [], idpBindingType: [Saml2BindingType.HttpRedirect],
spOutboundSigningAlgorithm: [], idpSingleSignOnServiceUrl: [],
spSigningBehavior: [], idpSingleLogoutServiceUrl: [],
spMinIncomingSigningAlgorithm: [], idpX509PublicCert: ["", dirtyRequired],
spWantAssertionsSigned: [], idpOutboundSigningAlgorithm: [defaultSigningAlgorithm],
spValidateCertificates: [], idpAllowUnsolicitedAuthnResponse: [],
idpAllowOutboundLogoutRequests: [true],
idpWantAuthnRequestsSigned: [],
},
{
updateOn: "blur",
}
);
idpEntityId: [], ssoConfigForm = this.formBuilder.group({
idpBindingType: [], configType: [SsoType.None],
idpSingleSignOnServiceUrl: [], keyConnectorEnabled: [false],
idpSingleLogoutServiceUrl: [], keyConnectorUrl: [""],
idpX509PublicCert: [], openId: this.openIdForm,
idpOutboundSigningAlgorithm: [], saml: this.samlForm,
idpAllowUnsolicitedAuthnResponse: [],
idpDisableOutboundLogoutRequests: [],
idpWantAuthnRequestsSigned: [],
}); });
constructor( constructor(
@@ -82,6 +151,25 @@ export class SsoComponent implements OnInit {
) {} ) {}
async ngOnInit() { 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.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId; this.organizationId = params.organizationId;
await this.load(); await this.load();
@@ -91,9 +179,7 @@ export class SsoComponent implements OnInit {
async load() { async load() {
this.organization = await this.organizationService.get(this.organizationId); this.organization = await this.organizationService.get(this.organizationId);
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId); const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
this.populateForm(ssoSettings);
this.data.patchValue(ssoSettings.data);
this.enabled.setValue(ssoSettings.enabled);
this.callbackPath = ssoSettings.urls.callbackPath; this.callbackPath = ssoSettings.urls.callbackPath;
this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath; this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath;
@@ -101,28 +187,30 @@ export class SsoComponent implements OnInit {
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl; this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
this.spAcsUrl = ssoSettings.urls.spAcsUrl; this.spAcsUrl = ssoSettings.urls.spAcsUrl;
this.keyConnectorUrl.markAsDirty();
this.loading = false; this.loading = false;
} }
copy(value: string) {
this.platformUtilsService.copyToClipboard(value);
}
launchUri(url: string) {
this.platformUtilsService.launchUri(url);
}
async submit() { 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 { try {
const response = await this.formPromise; const response = await this.formPromise;
this.populateForm(response);
this.data.patchValue(response.data);
this.enabled.setValue(response.enabled);
this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved")); this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved"));
} catch { } catch {
// Logged by appApiAction, do nothing // Logged by appApiAction, do nothing
@@ -131,24 +219,8 @@ export class SsoComponent implements OnInit {
this.formPromise = null; 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() { async validateKeyConnectorUrl() {
if (this.keyConnectorUrl.pristine) { if (this.haveTestedKeyConnector) {
return; 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() { get enableTestKeyConnector() {
return ( return (
this.data.get("keyConnectorEnabled").value && this.ssoConfigForm.get("keyConnectorEnabled").value &&
this.keyConnectorUrl != null && !Utils.isNullOrWhitespace(this.keyConnectorUrl?.value)
this.keyConnectorUrl.value !== ""
); );
} }
get keyConnectorUrl() { 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);
} }
} }

View File

@@ -4,11 +4,23 @@ import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { OssModule } from "src/app/oss.module"; 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 { SsoComponent } from "./manage/sso.component";
import { OrganizationsRoutingModule } from "./organizations-routing.module"; 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({ @NgModule({
imports: [CommonModule, FormsModule, ReactiveFormsModule, OssModule, OrganizationsRoutingModule], imports: [CommonModule, FormsModule, ReactiveFormsModule, OssModule, OrganizationsRoutingModule],
declarations: [SsoComponent], declarations: [
InputCheckboxComponent,
InputTextComponent,
InputTextReadOnlyComponent,
SelectComponent,
SsoComponent,
],
}) })
export class OrganizationsModule {} export class OrganizationsModule {}

2
jslib

Submodule jslib updated: a69135ce06...adfc2f234d

View File

@@ -61,12 +61,14 @@ import { CalloutComponent } from "jslib-angular/components/callout.component";
import { ExportScopeCalloutComponent } from "jslib-angular/components/export-scope-callout.component"; import { ExportScopeCalloutComponent } from "jslib-angular/components/export-scope-callout.component";
import { IconComponent } from "jslib-angular/components/icon.component"; import { IconComponent } from "jslib-angular/components/icon.component";
import { VerifyMasterPasswordComponent } from "jslib-angular/components/verify-master-password.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 { A11yTitleDirective } from "jslib-angular/directives/a11y-title.directive";
import { ApiActionDirective } from "jslib-angular/directives/api-action.directive"; import { ApiActionDirective } from "jslib-angular/directives/api-action.directive";
import { AutofocusDirective } from "jslib-angular/directives/autofocus.directive"; import { AutofocusDirective } from "jslib-angular/directives/autofocus.directive";
import { BlurClickDirective } from "jslib-angular/directives/blur-click.directive"; import { BlurClickDirective } from "jslib-angular/directives/blur-click.directive";
import { BoxRowDirective } from "jslib-angular/directives/box-row.directive"; import { BoxRowDirective } from "jslib-angular/directives/box-row.directive";
import { FallbackSrcDirective } from "jslib-angular/directives/fallback-src.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 { InputVerbatimDirective } from "jslib-angular/directives/input-verbatim.directive";
import { SelectCopyDirective } from "jslib-angular/directives/select-copy.directive"; import { SelectCopyDirective } from "jslib-angular/directives/select-copy.directive";
import { StopClickDirective } from "jslib-angular/directives/stop-click.directive"; import { StopClickDirective } from "jslib-angular/directives/stop-click.directive";
@@ -293,17 +295,18 @@ registerLocaleData(localeZhTw, "zh-TW");
], ],
declarations: [ declarations: [
A11yTitleDirective, A11yTitleDirective,
A11yInvalidDirective,
AcceptEmergencyComponent, AcceptEmergencyComponent,
AccessComponent,
AcceptOrganizationComponent, AcceptOrganizationComponent,
AccessComponent,
AccountComponent, AccountComponent,
SetPasswordComponent,
AddCreditComponent, AddCreditComponent,
AddEditComponent, AddEditComponent,
AddEditCustomFieldsComponent, AddEditCustomFieldsComponent,
AddEditCustomFieldsComponent,
AdjustPaymentComponent, AdjustPaymentComponent,
AdjustSubscription,
AdjustStorageComponent, AdjustStorageComponent,
AdjustSubscription,
ApiActionDirective, ApiActionDirective,
ApiKeyComponent, ApiKeyComponent,
AttachmentsComponent, AttachmentsComponent,
@@ -329,6 +332,7 @@ registerLocaleData(localeZhTw, "zh-TW");
DeauthorizeSessionsComponent, DeauthorizeSessionsComponent,
DeleteAccountComponent, DeleteAccountComponent,
DeleteOrganizationComponent, DeleteOrganizationComponent,
DisableSendPolicyComponent,
DomainRulesComponent, DomainRulesComponent,
DownloadLicenseComponent, DownloadLicenseComponent,
EmergencyAccessAddEditComponent, EmergencyAccessAddEditComponent,
@@ -352,22 +356,26 @@ registerLocaleData(localeZhTw, "zh-TW");
IconComponent, IconComponent,
ImportComponent, ImportComponent,
InactiveTwoFactorReportComponent, InactiveTwoFactorReportComponent,
InputStripSpacesDirective,
InputVerbatimDirective, InputVerbatimDirective,
LinkSsoComponent, LinkSsoComponent,
LockComponent, LockComponent,
LoginComponent, LoginComponent,
MasterPasswordPolicyComponent,
NavbarComponent, NavbarComponent,
NestedCheckboxComponent, NestedCheckboxComponent,
OptionsComponent, OptionsComponent,
OrgAccountComponent, OrgAccountComponent,
OrgAddEditComponent, OrgAddEditComponent,
OrganizationBillingComponent, OrganizationBillingComponent,
OrganizationLayoutComponent,
OrganizationPlansComponent, OrganizationPlansComponent,
OrganizationsComponent,
OrganizationSubscriptionComponent, OrganizationSubscriptionComponent,
OrgAttachmentsComponent, OrgAttachmentsComponent,
OrgBulkStatusComponent,
OrgBulkConfirmComponent, OrgBulkConfirmComponent,
OrgBulkRemoveComponent, OrgBulkRemoveComponent,
OrgBulkStatusComponent,
OrgCiphersComponent, OrgCiphersComponent,
OrgCollectionAddEditComponent, OrgCollectionAddEditComponent,
OrgCollectionsComponent, OrgCollectionsComponent,
@@ -376,49 +384,56 @@ registerLocaleData(localeZhTw, "zh-TW");
OrgEventsComponent, OrgEventsComponent,
OrgExportComponent, OrgExportComponent,
OrgExposedPasswordsReportComponent, OrgExposedPasswordsReportComponent,
OrgImportComponent,
OrgInactiveTwoFactorReportComponent,
OrgGroupAddEditComponent, OrgGroupAddEditComponent,
OrgGroupingsComponent, OrgGroupingsComponent,
OrgGroupsComponent, OrgGroupsComponent,
OrgImportComponent,
OrgInactiveTwoFactorReportComponent,
OrgManageCollectionsComponent, OrgManageCollectionsComponent,
OrgManageComponent, OrgManageComponent,
OrgPeopleComponent, OrgPeopleComponent,
OrgPolicyEditComponent,
OrgPoliciesComponent, OrgPoliciesComponent,
OrgPolicyEditComponent,
OrgResetPasswordComponent, OrgResetPasswordComponent,
OrgReusedPasswordsReportComponent, OrgReusedPasswordsReportComponent,
OrgSettingComponent, OrgSettingComponent,
OrgToolsComponent, OrgToolsComponent,
OrgTwoFactorSetupComponent, OrgTwoFactorSetupComponent,
OrgUnsecuredWebsitesReportComponent,
OrgUserAddEditComponent, OrgUserAddEditComponent,
OrgUserConfirmComponent, OrgUserConfirmComponent,
OrgUserGroupsComponent, OrgUserGroupsComponent,
OrganizationsComponent,
OrganizationLayoutComponent,
OrgUnsecuredWebsitesReportComponent,
OrgVaultComponent, OrgVaultComponent,
OrgWeakPasswordsReportComponent, OrgWeakPasswordsReportComponent,
PasswordGeneratorComponent, PasswordGeneratorComponent,
PasswordGeneratorHistoryComponent, PasswordGeneratorHistoryComponent,
PasswordStrengthComponent, PasswordGeneratorPolicyComponent,
PasswordRepromptComponent, PasswordRepromptComponent,
PasswordStrengthComponent,
PaymentComponent, PaymentComponent,
PersonalOwnershipPolicyComponent,
PremiumComponent, PremiumComponent,
ProfileComponent, ProfileComponent,
ProvidersComponent,
PurgeVaultComponent, PurgeVaultComponent,
RecoverDeleteComponent, RecoverDeleteComponent,
RecoverTwoFactorComponent, RecoverTwoFactorComponent,
RegisterComponent, RegisterComponent,
RemovePasswordComponent,
RequireSsoPolicyComponent,
ResetPasswordPolicyComponent,
ReusedPasswordsReportComponent, ReusedPasswordsReportComponent,
SearchCiphersPipe, SearchCiphersPipe,
SearchPipe, SearchPipe,
SelectCopyDirective, SelectCopyDirective,
SendAddEditComponent, SendAddEditComponent,
SendEffluxDatesComponent,
SendComponent, SendComponent,
SendEffluxDatesComponent,
SendOptionsPolicyComponent,
SetPasswordComponent,
SettingsComponent, SettingsComponent,
ShareComponent, ShareComponent,
SingleOrgPolicyComponent,
SponsoredFamiliesComponent, SponsoredFamiliesComponent,
SponsoringOrgRowComponent, SponsoringOrgRowComponent,
SsoComponent, SsoComponent,
@@ -427,6 +442,7 @@ registerLocaleData(localeZhTw, "zh-TW");
TaxInfoComponent, TaxInfoComponent,
ToolsComponent, ToolsComponent,
TrueFalseValueDirective, TrueFalseValueDirective,
TwoFactorAuthenticationPolicyComponent,
TwoFactorAuthenticatorComponent, TwoFactorAuthenticatorComponent,
TwoFactorComponent, TwoFactorComponent,
TwoFactorDuoComponent, TwoFactorDuoComponent,
@@ -444,41 +460,31 @@ registerLocaleData(localeZhTw, "zh-TW");
UpdatePasswordComponent, UpdatePasswordComponent,
UserBillingComponent, UserBillingComponent,
UserLayoutComponent, UserLayoutComponent,
UserSubscriptionComponent,
UserNamePipe, UserNamePipe,
UserSubscriptionComponent,
VaultComponent, VaultComponent,
VaultTimeoutInputComponent,
VerifyEmailComponent, VerifyEmailComponent,
VerifyEmailTokenComponent, VerifyEmailTokenComponent,
VerifyMasterPasswordComponent,
VerifyRecoverDeleteComponent, VerifyRecoverDeleteComponent,
WeakPasswordsReportComponent, WeakPasswordsReportComponent,
ProvidersComponent,
TwoFactorAuthenticationPolicyComponent,
MasterPasswordPolicyComponent,
SingleOrgPolicyComponent,
PasswordGeneratorPolicyComponent,
RequireSsoPolicyComponent,
PersonalOwnershipPolicyComponent,
DisableSendPolicyComponent,
SendOptionsPolicyComponent,
ResetPasswordPolicyComponent,
VaultTimeoutInputComponent,
AddEditCustomFieldsComponent,
VerifyMasterPasswordComponent,
RemovePasswordComponent,
], ],
exports: [ exports: [
A11yTitleDirective, A11yTitleDirective,
A11yInvalidDirective,
ApiActionDirective,
AvatarComponent, AvatarComponent,
CalloutComponent, CalloutComponent,
ApiActionDirective, FooterComponent,
I18nPipe,
InputStripSpacesDirective,
NavbarComponent,
OrganizationPlansComponent,
SearchPipe,
StopClickDirective, StopClickDirective,
StopPropDirective, StopPropDirective,
I18nPipe,
SearchPipe,
UserNamePipe, UserNamePipe,
NavbarComponent,
FooterComponent,
OrganizationPlansComponent,
], ],
providers: [DatePipe, SearchPipe, UserNamePipe], providers: [DatePipe, SearchPipe, UserNamePipe],
bootstrap: [], bootstrap: [],

View File

@@ -4414,25 +4414,25 @@
"message": "OIDC Redirect Behavior" "message": "OIDC Redirect Behavior"
}, },
"getClaimsFromUserInfoEndpoint": { "getClaimsFromUserInfoEndpoint": {
"message": "Get Claims From User Info Endpoint" "message": "Get claims from user info endpoint"
}, },
"additionalScopes": { "additionalScopes": {
"message": "Additional/Custom Scopes (comma delimited)" "message": "Custom Scopes"
}, },
"additionalUserIdClaimTypes": { "additionalUserIdClaimTypes": {
"message": "Additional/Custom User ID Claim Types (comma delimited)" "message": "Custom User ID Claim Types"
}, },
"additionalEmailClaimTypes": { "additionalEmailClaimTypes": {
"message": "Additional/Custom Email Claim Types (comma delimited)" "message": "Email Claim Types"
}, },
"additionalNameClaimTypes": { "additionalNameClaimTypes": {
"message": "Additional/Custom Name Claim Types (comma delimited)" "message": "Custom Name Claim Types"
}, },
"acrValues": { "acrValues": {
"message": "Requested Authentication Context Class Reference values (acr_values)" "message": "Requested Authentication Context Class Reference values"
}, },
"expectedReturnAcrValue": { "expectedReturnAcrValue": {
"message": "Expected \"acr\" Claim Value In Response (acr validation)" "message": "Expected \"acr\" Claim Value In Response"
}, },
"spEntityId": { "spEntityId": {
"message": "SP Entity ID" "message": "SP Entity ID"
@@ -4456,10 +4456,10 @@
"message": "Minimum Incoming Signing Algorithm" "message": "Minimum Incoming Signing Algorithm"
}, },
"spWantAssertionsSigned": { "spWantAssertionsSigned": {
"message": "Want Assertions Signed" "message": "Expect signed assertions"
}, },
"spValidateCertificates": { "spValidateCertificates": {
"message": "Validate Certificates" "message": "Validate certificates"
}, },
"idpEntityId": { "idpEntityId": {
"message": "Entity ID" "message": "Entity ID"
@@ -4473,9 +4473,6 @@
"idpSingleLogoutServiceUrl": { "idpSingleLogoutServiceUrl": {
"message": "Single Log Out Service URL" "message": "Single Log Out Service URL"
}, },
"idpArtifactResolutionServiceUrl": {
"message": "Artifact Resolution Service URL"
},
"idpX509PublicCert": { "idpX509PublicCert": {
"message": "X509 Public Certificate" "message": "X509 Public Certificate"
}, },
@@ -4483,13 +4480,13 @@
"message": "Outbound Signing Algorithm" "message": "Outbound Signing Algorithm"
}, },
"idpAllowUnsolicitedAuthnResponse": { "idpAllowUnsolicitedAuthnResponse": {
"message": "Allow Unsolicited Authentication Response" "message": "Allow unsolicited authentication response"
}, },
"idpDisableOutboundLogoutRequests": { "idpAllowOutboundLogoutRequests": {
"message": "Disable Outbound Logout Requests" "message": "Allow outbound logout requests"
}, },
"idpWantAuthnRequestsSigned": { "idpSignAuthenticationRequests": {
"message": "Want Authentication Requests Signed" "message": "Sign authentication requests"
}, },
"ssoSettingsSaved": { "ssoSettingsSaved": {
"message": "Single Sign-On configuration was saved." "message": "Single Sign-On configuration was saved."
@@ -4740,6 +4737,42 @@
"freeWithSponsorship": { "freeWithSponsorship": {
"message": "FREE with sponsorship" "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": { "sessionTimeout": {
"message": "Your session has timed out. Please go back and try logging in again." "message": "Your session has timed out. Please go back and try logging in again."
}, },

View File

@@ -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 // Browser specific icons overlayed on input fields. e.g. caps lock indicator on password field
::-webkit-calendar-picker-indicator, ::-webkit-calendar-picker-indicator,
input::-webkit-caps-lock-indicator, input::-webkit-caps-lock-indicator,