Refactor orgnaization policy management (#1147)

This commit is contained in:
Oscar Hinton 2021-08-25 16:10:17 +02:00 committed by GitHub
parent 8a259516df
commit 2cbe023a38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 687 additions and 437 deletions

View File

@ -20,10 +20,10 @@ import { ProviderUserType } from 'jslib-common/enums/providerUserType';
import { ValidationService } from 'jslib-angular/services/validation.service';
import { Organization } from 'jslib-common/models/domain/organization';
import {
ProviderOrganizationOrganizationDetailsResponse
} from 'jslib-common/models/response/provider/providerOrganizationResponse';
import { Organization } from 'jslib-common/models/domain/organization';
import { ModalComponent } from 'src/app/modal.component';
@ -88,7 +88,7 @@ export class ClientsComponent implements OnInit {
.map(o => o.id));
this.addableOrganizations = candidateOrgs.filter(o => allowedOrgsIds.includes(o.id));
this.showAddExisting = this.addableOrganizations.length != 0;
this.showAddExisting = this.addableOrganizations.length !== 0;
this.loading = false;
}

View File

@ -63,7 +63,7 @@ export class SetupComponent implements OnInit {
this.providerId = qParams.providerId;
this.token = qParams.token;
// Check if provider exists, redirect if it does
try {
const provider = await this.apiService.getProvider(this.providerId);

2
jslib

@ -1 +1 @@
Subproject commit 1f0127966e85aa29f9e50144de9b2a03b00de5d4
Subproject commit add4b2f3e9d85a4a68d67a0da09be14cefe9a6b3

View File

@ -46,8 +46,19 @@ import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.serv
import { ConstantsService } from 'jslib-common/services/constants.service';
import { PolicyListService } from './services/policy-list.service';
import { RouterService } from './services/router.service';
import { DisableSendPolicy } from './organizations/policies/disable-send.component';
import { MasterPasswordPolicy } from './organizations/policies/master-password.component';
import { PasswordGeneratorPolicy } from './organizations/policies/password-generator.component';
import { PersonalOwnershipPolicy } from './organizations/policies/personal-ownership.component';
import { RequireSsoPolicy } from './organizations/policies/require-sso.component';
import { ResetPasswordPolicy } from './organizations/policies/reset-password.component';
import { SendOptionsPolicy } from './organizations/policies/send-options.component';
import { SingleOrgPolicy } from './organizations/policies/single-org.component';
import { TwoFactorAuthenticationPolicy } from './organizations/policies/two-factor-authentication.component';
const BroadcasterSubscriptionId = 'AppComponent';
const IdleTimeout = 60000 * 10; // 10 minutes
@ -56,6 +67,7 @@ const IdleTimeout = 60000 * 10; // 10 minutes
templateUrl: 'app.component.html',
})
export class AppComponent implements OnDestroy, OnInit {
toasterConfig: ToasterConfig = new ToasterConfig({
showCloseButton: true,
mouseoverTimerStop: true,
@ -80,7 +92,7 @@ export class AppComponent implements OnDestroy, OnInit {
private sanitizer: DomSanitizer, private searchService: SearchService,
private notificationsService: NotificationsService, private routerService: RouterService,
private stateService: StateService, private eventService: EventService,
private policyService: PolicyService) { }
private policyService: PolicyService, protected policyListService: PolicyListService) { }
ngOnInit() {
this.ngZone.runOutsideAngular(() => {
@ -170,6 +182,18 @@ export class AppComponent implements OnDestroy, OnInit {
}
});
this.policyListService.addPolicies([
new TwoFactorAuthenticationPolicy(),
new MasterPasswordPolicy(),
new PasswordGeneratorPolicy(),
new SingleOrgPolicy(),
new RequireSsoPolicy(),
new PersonalOwnershipPolicy(),
new DisableSendPolicy(),
new SendOptionsPolicy(),
new ResetPasswordPolicy(),
]);
this.setFullWidth();
}

View File

@ -1,8 +1,3 @@
<app-callout *ngIf="userCanAccessBusinessPortal" [type]="'warning'">
<p>{{'webPoliciesDeprecationWarning' | i18n}}</p>
<button type="button" class="btn btn-outline-secondary"
(click)="goToEnterprisePortal()">{{'businessPortal' | i18n}}</button>
</app-callout>
<div class="page-header d-flex">
<h1>{{'policies' | i18n}}</h1>
</div>
@ -13,10 +8,10 @@
<table class="table table-hover table-list" *ngIf="!loading">
<tbody>
<tr *ngFor="let p of policies">
<td *ngIf="p.display">
<a href="#" appStopClick (click)="edit(p)">{{p.name}}</a>
<span class="badge badge-success" *ngIf="p.enabled">{{'enabled' | i18n}}</span>
<small class="text-muted d-block">{{p.description}}</small>
<td *ngIf="p.display(organization)">
<a href="#" appStopClick (click)="edit(p)">{{p.name | i18n}}</a>
<span class="badge badge-success" *ngIf="policiesEnabledMap.get(p.type)">{{'enabled' | i18n}}</span>
<small class="text-muted d-block">{{p.description | i18n}}</small>
</td>
</tr>
</tbody>

View File

@ -12,8 +12,8 @@ import {
import { PolicyType } from 'jslib-common/enums/policyType';
import { EnvironmentService } from 'jslib-common/abstractions';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserService } from 'jslib-common/abstractions/user.service';
@ -22,8 +22,13 @@ import { PolicyResponse } from 'jslib-common/models/response/policyResponse';
import { ModalComponent } from '../../modal.component';
import { Organization } from 'jslib-common/models/domain/organization';
import { PolicyEditComponent } from './policy-edit.component';
import { PolicyListService } from 'src/app/services/policy-list.service';
import { BasePolicy } from '../policies/base-policy.component';
@Component({
selector: 'app-org-policies',
templateUrl: 'policies.component.html',
@ -33,11 +38,11 @@ export class PoliciesComponent implements OnInit {
loading = true;
organizationId: string;
policies: any[];
policies: BasePolicy[];
organization: Organization;
// Remove when removing deprecation warning
enterpriseTokenPromise: Promise<any>;
userCanAccessBusinessPortal = false;
private enterpriseUrl: string;
@ -48,81 +53,20 @@ export class PoliciesComponent implements OnInit {
constructor(private apiService: ApiService, private route: ActivatedRoute,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private platformUtilsService: PlatformUtilsService, private userService: UserService,
private router: Router, private environmentService: EnvironmentService) { }
private policyListService: PolicyListService, private router: Router,
private environmentService: EnvironmentService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId);
if (organization == null || !organization.usePolicies) {
this.organization = await this.userService.getOrganization(this.organizationId);
if (this.organization == null || !this.organization.usePolicies) {
this.router.navigate(['/organizations', this.organizationId]);
return;
}
this.userCanAccessBusinessPortal = organization.canAccessBusinessPortal;
this.policies = [
{
name: this.i18nService.t('twoStepLogin'),
description: this.i18nService.t('twoStepLoginPolicyDesc'),
type: PolicyType.TwoFactorAuthentication,
enabled: false,
display: true,
},
{
name: this.i18nService.t('masterPass'),
description: this.i18nService.t('masterPassPolicyDesc'),
type: PolicyType.MasterPassword,
enabled: false,
display: true,
},
{
name: this.i18nService.t('passwordGenerator'),
description: this.i18nService.t('passwordGeneratorPolicyDesc'),
type: PolicyType.PasswordGenerator,
enabled: false,
display: true,
},
{
name: this.i18nService.t('singleOrg'),
description: this.i18nService.t('singleOrgDesc'),
type: PolicyType.SingleOrg,
enabled: false,
display: true,
},
{
name: this.i18nService.t('requireSso'),
description: this.i18nService.t('requireSsoPolicyDesc'),
type: PolicyType.RequireSso,
enabled: false,
display: organization.useSso,
},
{
name: this.i18nService.t('personalOwnership'),
description: this.i18nService.t('personalOwnershipPolicyDesc'),
type: PolicyType.PersonalOwnership,
enabled: false,
display: true,
},
{
name: this.i18nService.t('disableSend'),
description: this.i18nService.t('disableSendPolicyDesc'),
type: PolicyType.DisableSend,
enabled: false,
display: true,
},
{
name: this.i18nService.t('sendOptions'),
description: this.i18nService.t('sendOptionsPolicyDesc'),
type: PolicyType.SendOptions,
enabled: false,
display: true,
}, {
name: this.i18nService.t('resetPasswordPolicy'),
description: this.i18nService.t('resetPasswordPolicyDescription'),
type: PolicyType.ResetPassword,
enabled: false,
display: organization.useResetPassword,
},
];
this.policies = this.policyListService.getPolicies();
await this.load();
// Handle policies component launch from Event message
@ -158,13 +102,11 @@ export class PoliciesComponent implements OnInit {
this.orgPolicies.forEach(op => {
this.policiesEnabledMap.set(op.type, op.enabled);
});
this.policies.forEach(p => {
p.enabled = this.policiesEnabledMap.has(p.type) && this.policiesEnabledMap.get(p.type);
});
this.loading = false;
}
edit(p: any) {
edit(policy: BasePolicy) {
if (this.modal != null) {
this.modal.close();
}
@ -174,9 +116,7 @@ export class PoliciesComponent implements OnInit {
const childComponent = this.modal.show<PolicyEditComponent>(
PolicyEditComponent, this.editModalRef);
childComponent.name = p.name;
childComponent.description = p.description;
childComponent.type = p.type;
childComponent.policy = policy;
childComponent.organizationId = this.organizationId;
childComponent.policiesEnabledMap = this.policiesEnabledMap;
childComponent.onSavedPolicy.subscribe(() => {

View File

@ -2,178 +2,21 @@
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="policiesEditTitle">{{'editPolicy' | i18n}} - {{name}}</h2>
<h2 class="modal-title" id="policiesEditTitle">{{'editPolicy' | i18n}} - {{policy.name | i18n}}</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</div>
<div class="modal-body" *ngIf="!loading">
<p>{{description}}</p>
<app-callout type="warning" *ngIf="type === policyType.TwoFactorAuthentication"
title="{{'warning' | i18n}}" icon="fa-warning">
{{'twoStepLoginPolicyWarning' | i18n}}
</app-callout>
<app-callout type="warning" *ngIf="type === policyType.SingleOrg" title="{{'warning' | i18n}}"
icon="fa-warning">
{{'singleOrgPolicyWarning' | i18n}}
</app-callout>
<ng-container *ngIf="type === policyType.RequireSso">
<app-callout type="tip" title="{{'prerequisite' | i18n}}">
{{'requireSsoPolicyReq' | i18n}}
</app-callout>
<app-callout type="warning">
{{'requireSsoExemption' | i18n}}
</app-callout>
</ng-container>
<app-callout type="warning" *ngIf="type === policyType.PersonalOwnership">
{{'personalOwnershipExemption' | i18n}}
</app-callout>
<app-callout type="warning" *ngIf="type === policyType.DisableSend">
{{'disableSendExemption' | i18n}}
</app-callout>
<app-callout type="warning" *ngIf="type === policyType.SendOptions">
{{'sendOptionsExemption' | i18n}}
</app-callout>
<app-callout type="warning" *ngIf="type === policyType.ResetPassword">
{{'resetPasswordPolicyWarning' | i18n}}
</app-callout>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [(ngModel)]="enabled"
name="Enabled">
<label class="form-check-label" for="enabled">{{checkboxDesc}}</label>
</div>
<div class="modal-body">
<div class="modal-body" *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</div>
<div [hidden]="loading">
<p>{{policy.description | i18n}}</p>
<ng-template #policyForm></ng-template>
</div>
<ng-container *ngIf="type === policyType.MasterPassword">
<div class="row">
<div class="col-6 form-group">
<label for="masterPassMinComplexity">{{'minComplexityScore' | i18n}}</label>
<select id="masterPassMinComplexity" name="MasterPassMinComplexity"
[(ngModel)]="masterPassMinComplexity" class="form-control">
<option *ngFor="let o of passwordScores" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
<div class="col-6 form-group">
<label for="masterPassMinLength">{{'minLength' | i18n}}</label>
<input id="masterPassMinLength" class="form-control" type="number" min="8"
name="MasterPassMinLength" [(ngModel)]="masterPassMinLength">
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="masterPassRequireUpper"
[(ngModel)]="masterPassRequireUpper" name="MasterPassRequireUpper">
<label class="form-check-label" for="masterPassRequireUpper">A-Z</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="masterPassRequireLower"
[(ngModel)]="masterPassRequireLower" name="MasterPassRequireLower">
<label class="form-check-label" for="masterPassRequireLower">a-z</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="masterPassRequireNumbers"
[(ngModel)]="masterPassRequireNumbers" name="MasterPassRequireNumbers">
<label class="form-check-label" for="masterPassRequireNumbers">0-9</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="masterPassRequireSpecial"
[(ngModel)]="masterPassRequireSpecial" name="MasterPassRequireSpecial">
<label class="form-check-label" for="masterPassRequireSpecial">!@#$%^&amp;*</label>
</div>
</ng-container>
<ng-container *ngIf="type === policyType.PasswordGenerator">
<div class="row">
<div class="col-6 form-group mb-0">
<label for="passGenDefaultType">{{'defaultType' | i18n}}</label>
<select id="passGenDefaultType" name="PassGenDefaultType" [(ngModel)]="passGenDefaultType"
class="form-control">
<option *ngFor="let o of defaultTypes" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
</div>
<h3 class="mt-4">{{'password' | i18n}}</h3>
<div class="row">
<div class="col-6 form-group">
<label for="passGenMinLength">{{'minLength' | i18n}}</label>
<input id="passGenMinLength" class="form-control" type="number" name="PassGenMinLength"
min="5" max="128" [(ngModel)]="passGenMinLength">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="passGenMinNumbers">{{'minNumbers' | i18n}}</label>
<input id="passGenMinNumbers" class="form-control" type="number" name="PassGenMinNumbers"
min="0" max="9" [(ngModel)]="passGenMinNumbers">
</div>
<div class="col-6 form-group">
<label for="passGenMinSpecial">{{'minSpecial' | i18n}}</label>
<input id="passGenMinSpecial" class="form-control" type="number" name="PassGenMinSpecial"
min="0" max="9" [(ngModel)]="passGenMinSpecial">
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="passGenUseUpper"
[(ngModel)]="passGenUseUpper" name="PassGenUseUpper">
<label class="form-check-label" for="passGenUseUpper">A-Z</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="passGenUseLower"
[(ngModel)]="passGenUseLower" name="PassGenUseLower">
<label class="form-check-label" for="passGenUseLower">a-z</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="passGenUseNumbers"
[(ngModel)]="passGenUseNumbers" name="PassGenUseNumbers">
<label class="form-check-label" for="passGenUseNumbers">0-9</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="passGenUseSpecial"
[(ngModel)]="passGenUseSpecial" name="PassGenUseSpecial">
<label class="form-check-label" for="passGenUseSpecial">!@#$%^&amp;*</label>
</div>
<h3 class="mt-4">{{'passphrase' | i18n}}</h3>
<div class="row">
<div class="col-6 form-group">
<label for="passGenMinNumberWords">{{'minimumNumberOfWords' | i18n}}</label>
<input id="passGenMinNumberWords" class="form-control" type="number"
name="PassGenMinNumberWords" min="3" max="20" [(ngModel)]="passGenMinNumberWords">
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="passGenCapitalize"
[(ngModel)]="passGenCapitalize" name="PassGenCapitalize">
<label class="form-check-label" for="passGenCapitalize">{{'capitalize' | i18n}}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="passGenIncludeNumber"
[(ngModel)]="passGenIncludeNumber" name="PassGenIncludeNumber">
<label class="form-check-label" for="passGenIncludeNumber">{{'includeNumber' | i18n}}</label>
</div>
</ng-container>
<ng-container *ngIf="type === policyType.SendOptions">
<h3 class="mt-4">{{'options' | i18n}}</h3>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sendDisableHideEmail"
[(ngModel)]="sendDisableHideEmail" name="SendDisableHideEmail">
<label class="form-check-label" for="sendDisableHideEmail">{{'disableHideEmail' | i18n}}</label>
</div>
</ng-container>
<ng-container *ngIf="type === policyType.ResetPassword">
<h3 class="mt-4">{{'resetPasswordPolicyAutoEnroll' | i18n}}</h3>
<p>{{'resetPasswordPolicyAutoEnrollDescription' | i18n}}</p>
<app-callout type="warning">
{{'resetPasswordPolicyAutoEnrollWarning' | i18n}}
</app-callout>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="autoEnrollEnabled"
[(ngModel)]="resetPasswordAutoEnroll" name="AutoEnrollEnabled">
<label class="form-check-label"
for="autoEnrollEnabled">{{'resetPasswordPolicyAutoEnrollCheckbox' | i18n }}</label>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">

View File

@ -1,9 +1,12 @@
import {
ChangeDetectorRef,
Component,
ComponentFactoryResolver,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
@ -17,119 +20,52 @@ import { PolicyRequest } from 'jslib-common/models/request/policyRequest';
import { PolicyResponse } from 'jslib-common/models/response/policyResponse';
import { BasePolicy, BasePolicyComponent } from '../policies/base-policy.component';
@Component({
selector: 'app-policy-edit',
templateUrl: 'policy-edit.component.html',
})
export class PolicyEditComponent implements OnInit {
@Input() name: string;
@Input() description: string;
@Input() type: PolicyType;
export class PolicyEditComponent {
@Input() policy: BasePolicy;
@Input() organizationId: string;
@Input() policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
@Output() onSavedPolicy = new EventEmitter();
@ViewChild('policyForm', { read: ViewContainerRef, static: true }) policyFormRef: ViewContainerRef;
policyType = PolicyType;
loading = true;
enabled = false;
formPromise: Promise<any>;
passwordScores: any[];
defaultTypes: any[];
policyComponent: BasePolicyComponent;
// Master password
masterPassMinComplexity?: number = null;
masterPassMinLength?: number;
masterPassRequireUpper?: number;
masterPassRequireLower?: number;
masterPassRequireNumbers?: number;
masterPassRequireSpecial?: number;
// Password generator
passGenDefaultType?: string;
passGenMinLength?: number;
passGenUseUpper?: boolean;
passGenUseLower?: boolean;
passGenUseNumbers?: boolean;
passGenUseSpecial?: boolean;
passGenMinNumbers?: number;
passGenMinSpecial?: number;
passGenMinNumberWords?: number;
passGenCapitalize?: boolean;
passGenIncludeNumber?: boolean;
// Send options
sendDisableHideEmail?: boolean;
// Reset Password
resetPasswordAutoEnroll?: boolean;
private policy: PolicyResponse;
private policyResponse: PolicyResponse;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService) {
this.passwordScores = [
{ name: '-- ' + i18nService.t('select') + ' --', value: null },
{ name: i18nService.t('weak') + ' (0)', value: 0 },
{ name: i18nService.t('weak') + ' (1)', value: 1 },
{ name: i18nService.t('weak') + ' (2)', value: 2 },
{ name: i18nService.t('good') + ' (3)', value: 3 },
{ name: i18nService.t('strong') + ' (4)', value: 4 },
];
this.defaultTypes = [
{ name: i18nService.t('userPreference'), value: null },
{ name: i18nService.t('password'), value: 'password' },
{ name: i18nService.t('passphrase'), value: 'passphrase' },
];
private toasterService: ToasterService, private componentFactoryResolver: ComponentFactoryResolver,
private cdr: ChangeDetectorRef) {
}
async ngOnInit() {
async ngAfterViewInit() {
await this.load();
this.loading = false;
const factory = this.componentFactoryResolver.resolveComponentFactory(this.policy.component);
this.policyComponent = this.policyFormRef.createComponent(factory).instance as BasePolicyComponent;
this.policyComponent.policy = this.policy;
this.policyComponent.policyResponse = this.policyResponse;
this.cdr.detectChanges();
}
async load() {
try {
this.policy = await this.apiService.getPolicy(this.organizationId, this.type);
if (this.policy != null) {
this.enabled = this.policy.enabled;
if (this.policy.data != null) {
switch (this.type) {
case PolicyType.PasswordGenerator:
this.passGenDefaultType = this.policy.data.defaultType;
this.passGenMinLength = this.policy.data.minLength;
this.passGenUseUpper = this.policy.data.useUpper;
this.passGenUseLower = this.policy.data.useLower;
this.passGenUseNumbers = this.policy.data.useNumbers;
this.passGenUseSpecial = this.policy.data.useSpecial;
this.passGenMinNumbers = this.policy.data.minNumbers;
this.passGenMinSpecial = this.policy.data.minSpecial;
this.passGenMinNumberWords = this.policy.data.minNumberWords;
this.passGenCapitalize = this.policy.data.capitalize;
this.passGenIncludeNumber = this.policy.data.includeNumber;
break;
case PolicyType.MasterPassword:
this.masterPassMinComplexity = this.policy.data.minComplexity;
this.masterPassMinLength = this.policy.data.minLength;
this.masterPassRequireUpper = this.policy.data.requireUpper;
this.masterPassRequireLower = this.policy.data.requireLower;
this.masterPassRequireNumbers = this.policy.data.requireNumbers;
this.masterPassRequireSpecial = this.policy.data.requireSpecial;
break;
case PolicyType.SendOptions:
this.sendDisableHideEmail = this.policy.data.disableHideEmail;
break;
case PolicyType.ResetPassword:
this.resetPasswordAutoEnroll = this.policy.data.autoEnrollEnabled;
break;
default:
break;
}
}
}
this.policyResponse = await this.apiService.getPolicy(this.organizationId, this.policy.type);
} catch (e) {
if (e.statusCode === 404) {
this.enabled = false;
this.policyResponse = new PolicyResponse({Enabled: false});
} else {
throw e;
}
@ -137,94 +73,19 @@ export class PolicyEditComponent implements OnInit {
}
async submit() {
if (this.preValidate()) {
const request = new PolicyRequest();
request.enabled = this.enabled;
request.type = this.type;
request.data = null;
switch (this.type) {
case PolicyType.PasswordGenerator:
request.data = {
defaultType: this.passGenDefaultType,
minLength: this.passGenMinLength || null,
useUpper: this.passGenUseUpper,
useLower: this.passGenUseLower,
useNumbers: this.passGenUseNumbers,
useSpecial: this.passGenUseSpecial,
minNumbers: this.passGenMinNumbers || null,
minSpecial: this.passGenMinSpecial || null,
minNumberWords: this.passGenMinNumberWords || null,
capitalize: this.passGenCapitalize,
includeNumber: this.passGenIncludeNumber,
};
break;
case PolicyType.MasterPassword:
request.data = {
minComplexity: this.masterPassMinComplexity || null,
minLength: this.masterPassMinLength || null,
requireUpper: this.masterPassRequireUpper,
requireLower: this.masterPassRequireLower,
requireNumbers: this.masterPassRequireNumbers,
requireSpecial: this.masterPassRequireSpecial,
};
break;
case PolicyType.SendOptions:
request.data = {
disableHideEmail: this.sendDisableHideEmail,
};
break;
case PolicyType.ResetPassword:
request.data = {
autoEnrollEnabled: this.resetPasswordAutoEnroll,
};
break;
default:
break;
}
try {
this.formPromise = this.apiService.putPolicy(this.organizationId, this.type, request);
await this.formPromise;
this.toasterService.popAsync('success', null, this.i18nService.t('editedPolicyId', this.name));
this.onSavedPolicy.emit();
} catch { }
let request: PolicyRequest;
try {
request = await this.policyComponent.buildRequest(this.policiesEnabledMap);
} catch (e) {
this.toasterService.pop('error', null, e);
return;
}
}
get checkboxDesc(): string {
return this.type === PolicyType.PersonalOwnership ? this.i18nService.t('personalOwnershipCheckboxDesc') :
this.i18nService.t('enabled');
}
private preValidate(): boolean {
switch (this.type) {
case PolicyType.RequireSso:
// Don't need prevalidation checks if submitting to disable
if (!this.enabled) {
return true;
}
// Have SingleOrg policy enabled?
if (!(this.policiesEnabledMap.has(PolicyType.SingleOrg)
&& this.policiesEnabledMap.get(PolicyType.SingleOrg))) {
this.toasterService.popAsync('error', null, this.i18nService.t('requireSsoPolicyReqError'));
return false;
}
return true;
case PolicyType.SingleOrg:
// Don't need prevalidation checks if submitting to enable
if (this.enabled) {
return true;
}
// If RequireSso Policy is enabled prevent submittal
if (this.policiesEnabledMap.has(PolicyType.RequireSso)
&& this.policiesEnabledMap.get(PolicyType.RequireSso)) {
this.toasterService.popAsync('error', null, this.i18nService.t('disableRequireSsoError'));
return false;
}
return true;
default:
return true;
}
try {
this.formPromise = this.apiService.putPolicy(this.organizationId, this.policy.type, request);
await this.formPromise;
this.toasterService.popAsync('success', null, this.i18nService.t('editedPolicyId', this.i18nService.t(this.policy.name)));
this.onSavedPolicy.emit();
} catch {}
}
}

View File

@ -0,0 +1,54 @@
import {
Directive,
Input,
OnInit,
} from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Organization } from 'jslib-common/models/domain/organization';
import { PolicyType } from 'jslib-common/enums/policyType';
import { PolicyRequest } from 'jslib-common/models/request/policyRequest';
import { PolicyResponse } from 'jslib-common/models/response/policyResponse';
export abstract class BasePolicy {
abstract name: string;
abstract description: string;
abstract type: PolicyType;
abstract component: any;
display(organization: Organization) {
return true;
}
}
@Directive()
export abstract class BasePolicyComponent implements OnInit {
@Input() policyResponse: PolicyResponse;
@Input() policy: BasePolicy;
enabled = new FormControl(false);
data: FormGroup = null;
ngOnInit(): void {
this.enabled.setValue(this.policyResponse.enabled);
if (this.data != null) {
this.data.patchValue(this.policyResponse.data ?? {});
}
}
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>) {
const request = new PolicyRequest();
request.enabled = this.enabled.value;
request.type = this.policy.type;
if (this.data != null) {
request.data = this.data.value;
}
return Promise.resolve(request);
}
}

View File

@ -0,0 +1,10 @@
<app-callout type="warning">
{{'disableSendExemption' | i18n}}
</app-callout>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
</div>
</div>

View File

@ -0,0 +1,19 @@
import { Component } from '@angular/core';
import { PolicyType } from 'jslib-common/enums/policyType';
import { BasePolicy, BasePolicyComponent } from './base-policy.component';
export class DisableSendPolicy extends BasePolicy {
name = 'disableSend';
description = 'disableSendPolicyDesc';
type = PolicyType.DisableSend;
component = DisableSendPolicyComponent;
}
@Component({
selector: 'policy-disable-send',
templateUrl: 'disable-send.component.html',
})
export class DisableSendPolicyComponent extends BasePolicyComponent {
}

View File

@ -0,0 +1,42 @@
<div [formGroup]="data">
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="minComplexity">{{'minComplexityScore' | i18n}}</label>
<select id="minComplexity" name="minComplexity" formControlName="minComplexity" class="form-control">
<option *ngFor="let o of passwordScores" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
<div class="col-6 form-group">
<label for="minLength">{{'minLength' | i18n}}</label>
<input id="minLength" class="form-control" type="number" min="8" name="minLength"
formControlName="minLength">
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="requireUpper" name="requireUpper"
formControlName="requireUpper">
<label class="form-check-label" for="requireUpper">A-Z</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="requireLower" name="requireLower"
formControlName="requireLower">
<label class="form-check-label" for="requireLower">a-z</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="requireNumbers" name="requireNumbers"
formControlName="requireNumbers">
<label class="form-check-label" for="requireNumbers">0-9</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="requireSpecial" name="requireSpecial"
formControlName="requireSpecial">
<label class="form-check-label" for="requireSpecial">!@#$%^&amp;*</label>
</div>
</div>

View File

@ -0,0 +1,46 @@
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PolicyType } from 'jslib-common/enums/policyType';
import { BasePolicy, BasePolicyComponent } from './base-policy.component';
export class MasterPasswordPolicy extends BasePolicy {
name = 'masterPass';
description = 'masterPassPolicyDesc';
type = PolicyType.MasterPassword;
component = MasterPasswordPolicyComponent;
}
@Component({
selector: 'policy-master-password',
templateUrl: 'master-password.component.html',
})
export class MasterPasswordPolicyComponent extends BasePolicyComponent {
data = this.fb.group({
minComplexity: [null],
minLength: [null],
requireUpper: [null],
requireLower: [null],
requireNumbers: [null],
requireSpecial: [null],
});
passwordScores: { name: string; value: number; }[];
constructor(private fb: FormBuilder, i18nService: I18nService) {
super();
this.passwordScores = [
{ name: '-- ' + i18nService.t('select') + ' --', value: null },
{ name: i18nService.t('weak') + ' (0)', value: 0 },
{ name: i18nService.t('weak') + ' (1)', value: 1 },
{ name: i18nService.t('weak') + ' (2)', value: 2 },
{ name: i18nService.t('good') + ' (3)', value: 3 },
{ name: i18nService.t('strong') + ' (4)', value: 4 },
];
}
}

View File

@ -0,0 +1,72 @@
<div [formGroup]="data">
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
</div>
</div>
<div class="row">
<div class="col-6 form-group mb-0">
<label for="defaultType">{{'defaultType' | i18n}}</label>
<select id="defaultType" name="defaultType" formControlName="defaultType" class="form-control">
<option *ngFor="let o of defaultTypes" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
</div>
<h3 class="mt-4">{{'password' | i18n}}</h3>
<div class="row">
<div class="col-6 form-group">
<label for="minLength">{{'minLength' | i18n}}</label>
<input id="minLength" class="form-control" type="number" name="minLength" min="5" max="128"
formControlName="minLength">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="minNumbers">{{'minNumbers' | i18n}}</label>
<input id="minNumbers" class="form-control" type="number" name="minNumbers" min="0" max="9"
formControlName="minNumbers">
</div>
<div class="col-6 form-group">
<label for="minSpecial">{{'minSpecial' | i18n}}</label>
<input id="minSpecial" class="form-control" type="number" name="minSpecial" min="0" max="9"
formControlName="minSpecial">
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="useUpper"
formControlName="useUpper" name="useUpper">
<label class="form-check-label" for="useUpper">A-Z</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="useLower" name="useLower" formControlName="useLower">
<label class="form-check-label" for="useLower">a-z</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="useNumbers" name="useNumbers" formControlName="useNumbers">
<label class="form-check-label" for="useNumbers">0-9</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="useSpecial" name="useSpecial" formControlName="useSpecial">
<label class="form-check-label" for="useSpecial">!@#$%^&amp;*</label>
</div>
<h3 class="mt-4">{{'passphrase' | i18n}}</h3>
<div class="row">
<div class="col-6 form-group">
<label for="minNumberWords">{{'minimumNumberOfWords' | i18n}}</label>
<input id="minNumberWords" class="form-control" type="number" name="minNumberWords" min="3" max="20"
formControlName="minNumberWords">
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="capitalize" name="capitalize"
formControlName="capitalize">
<label class="form-check-label" for="capitalize">{{'capitalize' | i18n}}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="includeNumber" name="includeNumber"
formControlName="includeNumber">
<label class="form-check-label" for="includeNumber">{{'includeNumber' | i18n}}</label>
</div>
</div>

View File

@ -0,0 +1,48 @@
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PolicyType } from 'jslib-common/enums/policyType';
import { BasePolicy, BasePolicyComponent } from './base-policy.component';
export class PasswordGeneratorPolicy extends BasePolicy {
name = 'passwordGenerator';
description = 'passwordGeneratorPolicyDesc';
type = PolicyType.PasswordGenerator;
component = PasswordGeneratorPolicyComponent;
}
@Component({
selector: 'policy-password-generator',
templateUrl: 'password-generator.component.html',
})
export class PasswordGeneratorPolicyComponent extends BasePolicyComponent {
data = this.fb.group({
defaultType: [null],
minLength: [null],
useUpper: [null],
useLower: [null],
useNumbers: [null],
useSpecial: [null],
minNumbers: [null],
minSpecial: [null],
minNumberWords: [null],
capitalize: [null],
includeNumber: [null],
});
defaultTypes: { name: string; value: string; }[];
constructor(private fb: FormBuilder, i18nService: I18nService) {
super();
this.defaultTypes = [
{ name: i18nService.t('userPreference'), value: null },
{ name: i18nService.t('password'), value: 'password' },
{ name: i18nService.t('passphrase'), value: 'passphrase' },
];
}
}

View File

@ -0,0 +1,10 @@
<app-callout type="warning">
{{'personalOwnershipExemption' | i18n}}
</app-callout>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
<label class="form-check-label" for="enabled">{{'personalOwnershipCheckboxDesc' | i18n}}</label>
</div>
</div>

View File

@ -0,0 +1,19 @@
import { Component } from '@angular/core';
import { PolicyType } from 'jslib-common/enums/policyType';
import { BasePolicy, BasePolicyComponent } from './base-policy.component';
export class PersonalOwnershipPolicy extends BasePolicy {
name = 'personalOwnership';
description = 'personalOwnershipPolicyDesc';
type = PolicyType.PersonalOwnership;
component = PersonalOwnershipPolicyComponent;
}
@Component({
selector: 'policy-personal-ownership',
templateUrl: 'personal-ownership.component.html',
})
export class PersonalOwnershipPolicyComponent extends BasePolicyComponent {
}

View File

@ -0,0 +1,13 @@
<app-callout type="tip" title="{{'prerequisite' | i18n}}">
{{'requireSsoPolicyReq' | i18n}}
</app-callout>
<app-callout type="warning">
{{'requireSsoExemption' | i18n}}
</app-callout>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
</div>
</div>

View File

@ -0,0 +1,40 @@
import { Component } from '@angular/core';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PolicyType } from 'jslib-common/enums/policyType';
import { Organization } from 'jslib-common/models/domain/organization';
import { PolicyRequest } from 'jslib-common/models/request/policyRequest';
import { BasePolicy, BasePolicyComponent } from './base-policy.component';
export class RequireSsoPolicy extends BasePolicy {
name = 'requireSso';
description = 'requireSsoPolicyDesc';
type = PolicyType.RequireSso;
component = RequireSsoPolicyComponent;
display(organization: Organization) {
return organization.useSso;
}
}
@Component({
selector: 'policy-require-sso',
templateUrl: 'require-sso.component.html',
})
export class RequireSsoPolicyComponent extends BasePolicyComponent {
constructor(private i18nService: I18nService) {
super();
}
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false;
if (this.enabled.value && singleOrgEnabled) {
throw new Error(this.i18nService.t('requireSsoPolicyReqError'));
}
return super.buildRequest(policiesEnabledMap);
}
}

View File

@ -0,0 +1,25 @@
<app-callout type="warning">
{{'resetPasswordPolicyWarning' | i18n}}
</app-callout>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
</div>
</div>
<div [formGroup]="data">
<h3 class="mt-4">{{'resetPasswordPolicyAutoEnroll' | i18n}}</h3>
<p>{{'resetPasswordPolicyAutoEnrollDescription' | i18n}}</p>
<app-callout type="warning">
{{'resetPasswordPolicyAutoEnrollWarning' | i18n}}
</app-callout>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="autoEnrollEnabled" name="AutoEnrollEnabled"
formControlName="autoEnroll">
<label class="form-check-label" for="autoEnrollEnabled">
{{'resetPasswordPolicyAutoEnrollCheckbox' | i18n }}
</label>
</div>
</div>

View File

@ -0,0 +1,36 @@
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { PolicyType } from 'jslib-common/enums/policyType';
import { Organization } from 'jslib-common/models/domain/organization';
import { BasePolicy, BasePolicyComponent } from './base-policy.component';
export class ResetPasswordPolicy extends BasePolicy {
name = 'resetPasswordPolicy';
description = 'resetPasswordPolicyDescription';
type = PolicyType.ResetPassword;
component = ResetPasswordPolicyComponent;
display(organization: Organization) {
return organization.useResetPassword;
}
}
@Component({
selector: 'policy-reset-password',
templateUrl: 'reset-password.component.html',
})
export class ResetPasswordPolicyComponent extends BasePolicyComponent {
data = this.fb.group({
autoEnroll: false,
});
defaultTypes: { name: string; value: string; }[];
constructor(private fb: FormBuilder) {
super();
}
}

View File

@ -0,0 +1,19 @@
<app-callout type="warning">
{{'sendOptionsExemption' | i18n}}
</app-callout>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
</div>
</div>
<div [formGroup]="data">
<h3 class="mt-4">{{'options' | i18n}}</h3>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="disableHideEmail" name="DisableHideEmail"
formControlName="disableHideEmail">
<label class="form-check-label" for="disableHideEmail">{{'disableHideEmail' | i18n}}</label>
</div>
</div>

View File

@ -0,0 +1,28 @@
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { PolicyType } from 'jslib-common/enums/policyType';
import { BasePolicy, BasePolicyComponent } from './base-policy.component';
export class SendOptionsPolicy extends BasePolicy {
name = 'sendOptions';
description = 'sendOptionsPolicyDesc';
type = PolicyType.SendOptions;
component = SendOptionsPolicyComponent;
}
@Component({
selector: 'policy-send-options',
templateUrl: 'send-options.component.html',
})
export class SendOptionsPolicyComponent extends BasePolicyComponent {
data = this.fb.group({
disableHideEmail: false,
});
constructor(private fb: FormBuilder) {
super();
}
}

View File

@ -0,0 +1,10 @@
<app-callout type="warning">
{{'singleOrgPolicyWarning' | i18n}}
</app-callout>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
</div>
</div>

View File

@ -0,0 +1,36 @@
import { Component } from '@angular/core';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PolicyType } from 'jslib-common/enums/policyType';
import { PolicyRequest } from 'jslib-common/models/request/policyRequest';
import { BasePolicy, BasePolicyComponent } from './base-policy.component';
export class SingleOrgPolicy extends BasePolicy {
name = 'singleOrg';
description = 'singleOrgDesc';
type = PolicyType.SingleOrg;
component = SingleOrgPolicyComponent;
}
@Component({
selector: 'policy-single-org',
templateUrl: 'single-org.component.html',
})
export class SingleOrgPolicyComponent extends BasePolicyComponent {
constructor(private i18nService: I18nService) {
super();
}
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
const requireSsoEnabled = policiesEnabledMap.get(PolicyType.RequireSso) ?? false;
if (!this.enabled.value && requireSsoEnabled) {
throw new Error(this.i18nService.t('disableRequireSsoError'));
}
return super.buildRequest(policiesEnabledMap);
}
}

View File

@ -0,0 +1,10 @@
<app-callout type="warning">
{{'twoStepLoginPolicyWarning' | i18n}}
</app-callout>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
</div>
</div>

View File

@ -0,0 +1,19 @@
import { Component } from '@angular/core';
import { PolicyType } from 'jslib-common/enums/policyType';
import { BasePolicy, BasePolicyComponent } from './base-policy.component';
export class TwoFactorAuthenticationPolicy extends BasePolicy {
name = 'twoStepLogin';
description = 'twoStepLoginPolicyDesc';
type = PolicyType.TwoFactorAuthentication;
component = TwoFactorAuthenticationPolicyComponent;
}
@Component({
selector: 'policy-two-factor-authentication',
templateUrl: 'two-factor-authentication.component.html',
})
export class TwoFactorAuthenticationPolicyComponent extends BasePolicyComponent {
}

View File

@ -234,6 +234,16 @@ import localeUk from '@angular/common/locales/uk';
import localeZhCn from '@angular/common/locales/zh-Hans';
import localeZhTw from '@angular/common/locales/zh-Hant';
import { DisableSendPolicyComponent } from './organizations/policies/disable-send.component';
import { MasterPasswordPolicyComponent } from './organizations/policies/master-password.component';
import { PasswordGeneratorPolicyComponent } from './organizations/policies/password-generator.component';
import { PersonalOwnershipPolicyComponent } from './organizations/policies/personal-ownership.component';
import { RequireSsoPolicyComponent } from './organizations/policies/require-sso.component';
import { ResetPasswordPolicyComponent } from './organizations/policies/reset-password.component';
import { SendOptionsPolicyComponent } from './organizations/policies/send-options.component';
import { SingleOrgPolicyComponent } from './organizations/policies/single-org.component';
import { TwoFactorAuthenticationPolicyComponent } from './organizations/policies/two-factor-authentication.component';
registerLocaleData(localeAz, 'az');
registerLocaleData(localeBg, 'bg');
registerLocaleData(localeCa, 'ca');
@ -438,6 +448,15 @@ registerLocaleData(localeZhTw, 'zh-TW');
VerifyRecoverDeleteComponent,
WeakPasswordsReportComponent,
ProvidersComponent,
TwoFactorAuthenticationPolicyComponent,
MasterPasswordPolicyComponent,
SingleOrgPolicyComponent,
PasswordGeneratorPolicyComponent,
RequireSsoPolicyComponent,
PersonalOwnershipPolicyComponent,
DisableSendPolicyComponent,
SendOptionsPolicyComponent,
ResetPasswordPolicyComponent,
],
exports: [
A11yTitleDirective,

View File

@ -0,0 +1,13 @@
import { BasePolicy } from '../organizations/policies/base-policy.component';
export class PolicyListService {
private policies: BasePolicy[] = [];
addPolicies(policies: BasePolicy[]) {
this.policies.push(...policies);
}
getPolicies(): BasePolicy[] {
return this.policies;
}
}

View File

@ -15,6 +15,7 @@ import { WebPlatformUtilsService } from '../../services/webPlatformUtils.service
import { EventService } from './event.service';
import { OrganizationGuardService } from './organization-guard.service';
import { OrganizationTypeGuardService } from './organization-type-guard.service';
import { PolicyListService } from './policy-list.service';
import { RouterService } from './router.service';
import { AuthGuardService } from 'jslib-angular/services/auth-guard.service';
@ -191,6 +192,7 @@ export function initFactory(): Function {
RouterService,
EventService,
LockGuardService,
PolicyListService,
{ provide: AuditServiceAbstraction, useValue: auditService },
{ provide: AuthServiceAbstraction, useValue: authService },
{ provide: CipherServiceAbstraction, useValue: cipherService },

View File

@ -3369,9 +3369,6 @@
"linkSso": {
"message": "Link SSO"
},
"webPoliciesDeprecationWarning": {
"message": "Policy configuration has been moved, and this page will soon be deprecated. Please click below to use the Business Portal policies page instead."
},
"singleOrg": {
"message": "Single Organization"
},