1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-23 19:53:43 +00:00

[PM-25306] Policy documentation and reorganization (#16193)

* Add README for adding policy UI in Admin Console
* Update existing policy UIs to be standalone
* Reorganize files and use barrel files
* Use token to inject policies into PolicyListService
This commit is contained in:
Thomas Rittson
2025-09-11 21:34:48 +10:00
committed by GitHub
parent 22cf55a23f
commit afe3cbd78f
57 changed files with 656 additions and 395 deletions

View File

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

View File

@@ -0,0 +1,234 @@
# Adding a New Policy in Admin Console
This README explains how to add a new policy type to the Admin Console. Policies are used to control organizational behavior and security settings for their members.
This README does not cover checking the policy status in order to enforce it in the domain code.
## Overview
Each policy consists of three main components:
1. **Policy Type Enum** - Defines the policy type identifier
2. **Policy Definition & Component** - Implements the UI and business logic
3. **Registration** - Registers the policy in the application
## Step 1: Adding the Enum
Add your new policy type to the `PolicyType` enum.
**Important**: You must also add the corresponding PolicyType enum value on the server.
Example:
```typescript
export enum PolicyType {
// ... existing policies
YourNewPolicy = 21, // Use the next available number
}
```
## Step 2: Creating the Policy Definition and Component
### Policy Licensing and Location
The location where you create your policy depends on its licensing:
- **Open Source (OSS) Policies**: Create in `apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/`
- **Bitwarden Licensed Policies**: Create in `bitwarden_license/bit-web/src/app/admin-console/policies/`
Most policies should be OSS licensed unless they specifically relate to premium/enterprise features that are part of Bitwarden's commercial offerings.
Create a new component file in the appropriate `policy-edit-definitions/` folder following the naming pattern `your-policy-name.component.ts`.
**Note:** you may also create the policy files in your own team's code if you prefer to own your own definition. The same licensing considerations apply.
### Basic Structure
```typescript
import { Component } from "@angular/core";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { SharedModule } from "../../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
// Policy Definition Class
export class YourNewPolicy extends BasePolicyEditDefinition {
name = "yourPolicyNameTitle"; // i18n key for title
description = "yourPolicyNameDesc"; // i18n key for description
type = PolicyType.YourNewPolicy; // Reference to enum
component = YourNewPolicyComponent; // Reference to component
}
// Policy Component Class
@Component({
templateUrl: "your-policy-name.component.html",
imports: [SharedModule],
})
export class YourNewPolicyComponent extends BasePolicyEditComponent {
// Component implementation
}
```
### Common Use Cases
#### Simple Toggle Policy (No Additional Configuration)
For policies that only need an enabled/disabled state:
```typescript
export class SimpleTogglePolicy extends BasePolicyEditDefinition {
name = "simpleTogglePolicyTitle";
description = "simpleTogglePolicyDesc";
type = PolicyType.SimpleToggle;
component = SimpleTogglePolicyComponent;
}
@Component({
templateUrl: "simple-toggle.component.html",
imports: [SharedModule],
})
export class SimpleTogglePolicyComponent extends BasePolicyEditComponent {}
```
Template (`simple-toggle.component.html`):
```html
<bit-form-control>
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
<bit-label>{{ "turnOn" | i18n }}</bit-label>
</bit-form-control>
```
#### Policy with Configuration Data
For policies requiring additional settings beyond just enabled/disabled, you'll need to define a custom `data` FormGroup to handle the policy's configuration options:
```typescript
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { ControlsOf } from "@bitwarden/angular/types/controls-of";
interface YourPolicyOptions {
minLength: number;
requireSpecialChar: boolean;
}
@Component({
templateUrl: "your-policy.component.html",
imports: [SharedModule],
})
export class YourPolicyComponent extends BasePolicyEditComponent implements OnInit {
data: FormGroup<ControlsOf<YourPolicyOptions>> = this.formBuilder.group({
minLength: [8, [Validators.min(1)]],
requireSpecialChar: [false],
});
constructor(private formBuilder: FormBuilder) {
super();
}
async ngOnInit() {
super.ngOnInit();
// Additional initialization logic
}
}
```
Template (`your-policy.component.html`):
```html
<bit-form-control>
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
<bit-label>{{ "turnOn" | i18n }}</bit-label>
</bit-form-control>
<ng-container [formGroup]="data">
<bit-form-field>
<bit-label>{{ "minimumLength" | i18n }}</bit-label>
<input bitInput type="number" formControlName="minLength" />
</bit-form-field>
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="requireSpecialChar" />
<bit-label>{{ "requireSpecialCharacter" | i18n }}</bit-label>
</bit-form-control>
</ng-container>
```
#### Feature Flagged Policy
To hide a policy behind a feature flag using ConfigService:
```typescript
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
export class NewPolicyBeta extends BasePolicyEditDefinition {
name = "newPolicyTitle";
description = "newPolicyDesc";
type = PolicyType.NewPolicy;
component = NewPolicyComponent;
// Only show if feature flag is enabled
display$(organization: Organization, configService: ConfigService) {
return configService.getFeatureFlag$(FeatureFlag.YourNewPolicyFeature);
}
}
```
#### Policy related to Organization Features
To show a policy only when the organization has a specific plan feature:
```typescript
import { of } from "rxjs";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
export class RequireSsoPolicy extends BasePolicyEditDefinition {
name = "requireSsoTitle";
description = "requireSsoDesc";
type = PolicyType.RequireSso;
component = RequireSsoPolicyComponent;
// Only show if organization has SSO enabled
display$(organization: Organization, configService: ConfigService) {
return of(organization.useSso);
}
}
```
## Step 3: Registering the Policy
### Export from Index File
Add your policy to the barrel file in its folder:
```typescript
export { YourNewPolicy } from "./your-policy-name.component";
```
### Register in Policy Register
Add your policy to the appropriate register in `policy-edit-register.ts`:
```typescript
import {
// ... existing imports
YourNewPolicy,
} from "./policy-edit-definitions";
export const ossPolicyEditRegister: BasePolicyEditDefinition[] = [
// ... existing policies
new YourNewPolicy(),
];
```
**Note**: Use `ossPolicyEditRegister` for open-source policies and `bitPolicyEditRegister` for Bitwarden Licensed policies.
## Testing Your Policy
1. Build and run the application
2. Navigate to Admin Console → Policies
3. Verify your policy appears in the list
4. Test the policy configuration UI
5. Verify policy data saves correctly

View File

@@ -0,0 +1,119 @@
import { Directive, Input, OnInit } from "@angular/core";
import { FormControl, UntypedFormGroup } from "@angular/forms";
import { Observable, of } from "rxjs";
import { Constructor } from "type-fest";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
/**
* A metadata class that defines how a policy is displayed in the Admin Console Policies page for editing.
* Add this to the `ossPolicyRegister` or `bitPolicyRegister` file to register it in the application.
*/
export abstract class BasePolicyEditDefinition {
/**
* i18n string for the policy name.
*/
abstract name: string;
/**
* i18n string for the policy description.
* This is shown in the list of policies.
*/
abstract description: string;
/**
* The PolicyType enum that this policy represents.
*/
abstract type: PolicyType;
/**
* The component used to edit this policy. See {@link BasePolicyEditComponent}.
*/
abstract component: Constructor<BasePolicyEditComponent>;
/**
* If true, the {@link description} will be reused in the policy edit modal. Set this to false if you
* have more complex requirements that you will implement in your template instead.
**/
showDescription: boolean = true;
/**
* A method that determines whether to display this policy in the Admin Console Policies page.
* The default implementation will always display the policy.
* This can be used to hide the policy based on the organization's plan features or a feature flag value.
* Note: this only hides the policy for editing in Admin Console, it does not affect its enforcement
* if it has already been turned on. Enforcement should be feature flagged separately.
*/
display$(organization: Organization, configService: ConfigService): Observable<boolean> {
return of(true);
}
}
/**
* A component used to edit the policy settings in Admin Console. It is rendered inside the PolicyEditDialogComponent.
* This should contain the form controls used to edit the policy (including the Enabled checkbox) and any additional
* warnings or callouts.
* See existing implementations as a guide.
*/
@Directive()
export abstract class BasePolicyEditComponent implements OnInit {
@Input() policyResponse: PolicyResponse | undefined;
@Input() policy: BasePolicyEditDefinition | undefined;
/**
* Whether the policy is enabled.
*/
enabled = new FormControl(false);
/**
* An optional FormGroup for additional policy configuration. Required for more complex policies only.
*/
data: UntypedFormGroup | undefined;
ngOnInit(): void {
this.enabled.setValue(this.policyResponse?.enabled ?? false);
if (this.policyResponse?.data != null) {
this.loadData();
}
}
buildRequest() {
if (!this.policy) {
throw new Error("Policy was not found");
}
const request: PolicyRequest = {
type: this.policy.type,
enabled: this.enabled.value ?? false,
data: this.buildRequestData(),
};
return Promise.resolve(request);
}
/**
* This is called before the policy is saved. If it returns false, it will not be saved
* and the user will remain on the policy edit dialog.
* This can be used to trigger an additional confirmation modal before saving.
* */
confirm(): Promise<boolean> | boolean {
return true;
}
protected loadData() {
this.data?.patchValue(this.policyResponse?.data ?? {});
}
/**
* Transforms the {@link data} FormGroup to the policy data model for saving.
*/
protected buildRequestData() {
if (this.data != null) {
return this.data.value;
}
return null;
}
}

View File

@@ -1,76 +0,0 @@
import { Directive, Input, OnInit } from "@angular/core";
import { UntypedFormControl, UntypedFormGroup } from "@angular/forms";
import { Observable, of } from "rxjs";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
export abstract class BasePolicy {
abstract name: string;
abstract description: string;
abstract type: PolicyType;
abstract component: any;
/**
* If true, the description will be reused in the policy edit modal. Set this to false if you
* have more complex requirements that you will implement in your template instead.
**/
showDescription: boolean = true;
display(organization: Organization, configService: ConfigService): Observable<boolean> {
return of(true);
}
}
@Directive()
export abstract class BasePolicyComponent implements OnInit {
@Input() policyResponse: PolicyResponse | undefined;
@Input() policy: BasePolicy | undefined;
enabled = new UntypedFormControl(false);
data: UntypedFormGroup | undefined;
ngOnInit(): void {
this.enabled.setValue(this.policyResponse?.enabled);
if (this.policyResponse?.data != null) {
this.loadData();
}
}
buildRequest() {
if (!this.policy) {
throw new Error("Policy was not found");
}
const request: PolicyRequest = {
type: this.policy.type,
enabled: this.enabled.value,
data: this.buildRequestData(),
};
return Promise.resolve(request);
}
/**
* Enable optional validation before sumitting a respose for policy submission
* */
confirm(): Promise<boolean> | boolean {
return true;
}
protected loadData() {
this.data?.patchValue(this.policyResponse?.data ?? {});
}
protected buildRequestData() {
if (this.data != null) {
return this.data.value;
}
return null;
}
}

View File

@@ -1,15 +1,4 @@
export * from "./policies.module";
export { BasePolicy, BasePolicyComponent } from "./base-policy.component";
export { DisableSendPolicy } from "./disable-send.component";
export { MasterPasswordPolicy } from "./master-password.component";
export { PasswordGeneratorPolicy } from "./password-generator.component";
export { vNextOrganizationDataOwnershipPolicy } from "./vnext-organization-data-ownership.component";
export { OrganizationDataOwnershipPolicy } from "./organization-data-ownership.component";
export { RequireSsoPolicy } from "./require-sso.component";
export { ResetPasswordPolicy } from "./reset-password.component";
export { SendOptionsPolicy } from "./send-options.component";
export { SingleOrgPolicy } from "./single-org.component";
export { TwoFactorAuthenticationPolicy } from "./two-factor-authentication.component";
export { PoliciesComponent } from "./policies.component";
export { RemoveUnlockWithPinPolicy } from "./remove-unlock-with-pin.component";
export { RestrictedItemTypesPolicy } from "./restricted-item-types.component";
export { ossPolicyEditRegister } from "./policy-edit-register";
export { BasePolicyEditDefinition, BasePolicyEditComponent } from "./base-policy-edit.component";
export { POLICY_EDIT_REGISTER } from "./policy-register-token";

View File

@@ -14,7 +14,7 @@
<bit-table>
<ng-template body>
@for (p of policies; track p.name) {
@if (p.display(organization, configService) | async) {
@if (p.display$(organization, configService) | async) {
<tr bitRow>
<td bitCell ngPreserveWhitespaces>
<button type="button" bitLink (click)="edit(p)">{{ p.name | i18n }}</button>

View File

@@ -16,22 +16,31 @@ import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { DialogService } from "@bitwarden/components";
import { safeProvider } from "@bitwarden/ui-common";
import { PolicyListService } from "../../core/policy-list.service";
import { BasePolicy } from "../policies";
import { HeaderModule } from "../../../layouts/header/header.module";
import { SharedModule } from "../../../shared";
import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component";
import { BasePolicyEditDefinition } from "./base-policy-edit.component";
import { PolicyEditDialogComponent } from "./policy-edit-dialog.component";
import { PolicyListService } from "./policy-list.service";
import { POLICY_EDIT_REGISTER } from "./policy-register-token";
@Component({
selector: "app-org-policies",
templateUrl: "policies.component.html",
standalone: false,
imports: [SharedModule, HeaderModule],
providers: [
safeProvider({
provide: PolicyListService,
deps: [POLICY_EDIT_REGISTER],
}),
],
})
export class PoliciesComponent implements OnInit {
loading = true;
organizationId: string;
policies: BasePolicy[];
organization$: Observable<Organization>;
policies: readonly BasePolicyEditDefinition[];
protected organization$: Observable<Organization>;
private orgPolicies: PolicyResponse[];
protected policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
@@ -97,8 +106,8 @@ export class PoliciesComponent implements OnInit {
this.loading = false;
}
async edit(policy: BasePolicy) {
const dialogRef = PolicyEditComponent.open(this.dialogService, {
async edit(policy: BasePolicyEditDefinition) {
const dialogRef = PolicyEditDialogComponent.open(this.dialogService, {
data: {
policy: policy,
organizationId: this.organizationId,
@@ -106,7 +115,7 @@ export class PoliciesComponent implements OnInit {
});
const result = await lastValueFrom(dialogRef.closed);
if (result === PolicyEditDialogResult.Saved) {
if (result == "saved") {
await this.load();
}
}

View File

@@ -1,52 +0,0 @@
import { NgModule } from "@angular/core";
import { HeaderModule } from "../../../layouts/header/header.module";
import { SharedModule } from "../../../shared";
import { DisableSendPolicyComponent } from "./disable-send.component";
import { MasterPasswordPolicyComponent } from "./master-password.component";
import { OrganizationDataOwnershipPolicyComponent } from "./organization-data-ownership.component";
import { PasswordGeneratorPolicyComponent } from "./password-generator.component";
import { PoliciesComponent } from "./policies.component";
import { PolicyEditComponent } from "./policy-edit.component";
import { RemoveUnlockWithPinPolicyComponent } from "./remove-unlock-with-pin.component";
import { RequireSsoPolicyComponent } from "./require-sso.component";
import { ResetPasswordPolicyComponent } from "./reset-password.component";
import { RestrictedItemTypesPolicyComponent } from "./restricted-item-types.component";
import { SendOptionsPolicyComponent } from "./send-options.component";
import { SingleOrgPolicyComponent } from "./single-org.component";
import { TwoFactorAuthenticationPolicyComponent } from "./two-factor-authentication.component";
@NgModule({
imports: [SharedModule, HeaderModule],
declarations: [
DisableSendPolicyComponent,
MasterPasswordPolicyComponent,
PasswordGeneratorPolicyComponent,
OrganizationDataOwnershipPolicyComponent,
RequireSsoPolicyComponent,
ResetPasswordPolicyComponent,
SendOptionsPolicyComponent,
SingleOrgPolicyComponent,
TwoFactorAuthenticationPolicyComponent,
PoliciesComponent,
PolicyEditComponent,
RemoveUnlockWithPinPolicyComponent,
RestrictedItemTypesPolicyComponent,
],
exports: [
DisableSendPolicyComponent,
MasterPasswordPolicyComponent,
PasswordGeneratorPolicyComponent,
OrganizationDataOwnershipPolicyComponent,
RequireSsoPolicyComponent,
ResetPasswordPolicyComponent,
SendOptionsPolicyComponent,
SingleOrgPolicyComponent,
TwoFactorAuthenticationPolicyComponent,
PoliciesComponent,
PolicyEditComponent,
RemoveUnlockWithPinPolicyComponent,
],
})
export class PoliciesModule {}

View File

@@ -2,9 +2,10 @@ import { Component } from "@angular/core";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
import { SharedModule } from "../../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
export class DisableSendPolicy extends BasePolicy {
export class DisableSendPolicy extends BasePolicyEditDefinition {
name = "disableSend";
description = "disableSendPolicyDesc";
type = PolicyType.DisableSend;
@@ -12,8 +13,7 @@ export class DisableSendPolicy extends BasePolicy {
}
@Component({
selector: "policy-disable-send",
templateUrl: "disable-send.component.html",
standalone: false,
imports: [SharedModule],
})
export class DisableSendPolicyComponent extends BasePolicyComponent {}
export class DisableSendPolicyComponent extends BasePolicyEditComponent {}

View File

@@ -0,0 +1,15 @@
export { DisableSendPolicy } from "./disable-send.component";
export { MasterPasswordPolicy } from "./master-password.component";
export { OrganizationDataOwnershipPolicy } from "./organization-data-ownership.component";
export { PasswordGeneratorPolicy } from "./password-generator.component";
export { RemoveUnlockWithPinPolicy } from "./remove-unlock-with-pin.component";
export { RequireSsoPolicy } from "./require-sso.component";
export { ResetPasswordPolicy } from "./reset-password.component";
export { RestrictedItemTypesPolicy } from "./restricted-item-types.component";
export { SendOptionsPolicy } from "./send-options.component";
export { SingleOrgPolicy } from "./single-org.component";
export { TwoFactorAuthenticationPolicy } from "./two-factor-authentication.component";
export {
vNextOrganizationDataOwnershipPolicy,
vNextOrganizationDataOwnershipPolicyComponent,
} from "./vnext-organization-data-ownership.component";

View File

@@ -16,9 +16,10 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
import { SharedModule } from "../../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
export class MasterPasswordPolicy extends BasePolicy {
export class MasterPasswordPolicy extends BasePolicyEditDefinition {
name = "masterPassPolicyTitle";
description = "masterPassPolicyDesc";
type = PolicyType.MasterPassword;
@@ -26,11 +27,10 @@ export class MasterPasswordPolicy extends BasePolicy {
}
@Component({
selector: "policy-master-password",
templateUrl: "master-password.component.html",
standalone: false,
imports: [SharedModule],
})
export class MasterPasswordPolicyComponent extends BasePolicyComponent implements OnInit {
export class MasterPasswordPolicyComponent extends BasePolicyEditComponent implements OnInit {
MinPasswordLength = Utils.minimumPasswordLength;
data: FormGroup<ControlsOf<MasterPasswordPolicyOptions>> = this.formBuilder.group({

View File

@@ -6,15 +6,16 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
import { SharedModule } from "../../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
export class OrganizationDataOwnershipPolicy extends BasePolicy {
export class OrganizationDataOwnershipPolicy extends BasePolicyEditDefinition {
name = "organizationDataOwnership";
description = "personalOwnershipPolicyDesc";
type = PolicyType.OrganizationDataOwnership;
component = OrganizationDataOwnershipPolicyComponent;
display(organization: Organization, configService: ConfigService): Observable<boolean> {
display$(organization: Organization, configService: ConfigService): Observable<boolean> {
return configService
.getFeatureFlag$(FeatureFlag.CreateDefaultLocation)
.pipe(map((enabled) => !enabled));
@@ -22,8 +23,7 @@ export class OrganizationDataOwnershipPolicy extends BasePolicy {
}
@Component({
selector: "policy-organization-data-ownership",
templateUrl: "organization-data-ownership.component.html",
standalone: false,
imports: [SharedModule],
})
export class OrganizationDataOwnershipPolicyComponent extends BasePolicyComponent {}
export class OrganizationDataOwnershipPolicyComponent extends BasePolicyEditComponent {}

View File

@@ -9,9 +9,10 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { BuiltIn, Profile } from "@bitwarden/generator-core";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
import { SharedModule } from "../../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
export class PasswordGeneratorPolicy extends BasePolicy {
export class PasswordGeneratorPolicy extends BasePolicyEditDefinition {
name = "passwordGenerator";
description = "passwordGeneratorPolicyDesc";
type = PolicyType.PasswordGenerator;
@@ -19,11 +20,10 @@ export class PasswordGeneratorPolicy extends BasePolicy {
}
@Component({
selector: "policy-password-generator",
templateUrl: "password-generator.component.html",
standalone: false,
imports: [SharedModule],
})
export class PasswordGeneratorPolicyComponent extends BasePolicyComponent {
export class PasswordGeneratorPolicyComponent extends BasePolicyEditComponent {
// these properties forward the application default settings to the UI
// for HTML attribute bindings
protected readonly minLengthMin =

View File

@@ -1,11 +1,8 @@
import { CommonModule } from "@angular/common";
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ReactiveFormsModule } from "@angular/forms";
import { By } from "@angular/platform-browser";
import { mock } from "jest-mock-extended";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -33,8 +30,6 @@ describe("RemoveUnlockWithPinPolicyComponent", () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CommonModule, ReactiveFormsModule],
declarations: [RemoveUnlockWithPinPolicyComponent, I18nPipe],
providers: [
{ provide: I18nService, useValue: mock<I18nService>() },
{ provide: I18nService, useValue: i18nService },
@@ -102,9 +97,6 @@ describe("RemoveUnlockWithPinPolicyComponent", () => {
const bitLabelElement = fixture.debugElement.query(By.css("bit-label"));
expect(bitLabelElement).not.toBeNull();
const textNodes = bitLabelElement.childNodes
.filter((node) => node.nativeNode.nodeType === Node.TEXT_NODE)
.map((node) => node.nativeNode.wholeText?.trim());
expect(textNodes).toContain("Turn on");
expect(bitLabelElement.nativeElement.textContent.trim()).toBe("Turn on");
});
});

View File

@@ -2,9 +2,10 @@ import { Component } from "@angular/core";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
import { SharedModule } from "../../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
export class RemoveUnlockWithPinPolicy extends BasePolicy {
export class RemoveUnlockWithPinPolicy extends BasePolicyEditDefinition {
name = "removeUnlockWithPinPolicyTitle";
description = "removeUnlockWithPinPolicyDesc";
type = PolicyType.RemoveUnlockWithPin;
@@ -12,8 +13,7 @@ export class RemoveUnlockWithPinPolicy extends BasePolicy {
}
@Component({
selector: "remove-unlock-with-pin",
templateUrl: "remove-unlock-with-pin.component.html",
standalone: false,
imports: [SharedModule],
})
export class RemoveUnlockWithPinPolicyComponent extends BasePolicyComponent {}
export class RemoveUnlockWithPinPolicyComponent extends BasePolicyEditComponent {}

View File

@@ -5,22 +5,22 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
import { SharedModule } from "../../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
export class RequireSsoPolicy extends BasePolicy {
export class RequireSsoPolicy extends BasePolicyEditDefinition {
name = "requireSso";
description = "requireSsoPolicyDesc";
type = PolicyType.RequireSso;
component = RequireSsoPolicyComponent;
display(organization: Organization, configService: ConfigService) {
display$(organization: Organization, configService: ConfigService) {
return of(organization.useSso);
}
}
@Component({
selector: "policy-require-sso",
templateUrl: "require-sso.component.html",
standalone: false,
imports: [SharedModule],
})
export class RequireSsoPolicyComponent extends BasePolicyComponent {}
export class RequireSsoPolicyComponent extends BasePolicyEditComponent {}

View File

@@ -12,25 +12,25 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
import { SharedModule } from "../../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
export class ResetPasswordPolicy extends BasePolicy {
export class ResetPasswordPolicy extends BasePolicyEditDefinition {
name = "accountRecoveryPolicy";
description = "accountRecoveryPolicyDesc";
type = PolicyType.ResetPassword;
component = ResetPasswordPolicyComponent;
display(organization: Organization, configService: ConfigService) {
display$(organization: Organization, configService: ConfigService) {
return of(organization.useResetPassword);
}
}
@Component({
selector: "policy-reset-password",
templateUrl: "reset-password.component.html",
standalone: false,
imports: [SharedModule],
})
export class ResetPasswordPolicyComponent extends BasePolicyComponent implements OnInit {
export class ResetPasswordPolicyComponent extends BasePolicyEditComponent implements OnInit {
data = this.formBuilder.group({
autoEnrollEnabled: false,
});

View File

@@ -6,25 +6,25 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
import { SharedModule } from "../../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
export class RestrictedItemTypesPolicy extends BasePolicy {
export class RestrictedItemTypesPolicy extends BasePolicyEditDefinition {
name = "restrictedItemTypePolicy";
description = "restrictedItemTypePolicyDesc";
type = PolicyType.RestrictedItemTypes;
component = RestrictedItemTypesPolicyComponent;
display(organization: Organization, configService: ConfigService): Observable<boolean> {
display$(organization: Organization, configService: ConfigService): Observable<boolean> {
return configService.getFeatureFlag$(FeatureFlag.RemoveCardItemTypePolicy);
}
}
@Component({
selector: "policy-restricted-item-types",
templateUrl: "restricted-item-types.component.html",
standalone: false,
imports: [SharedModule],
})
export class RestrictedItemTypesPolicyComponent extends BasePolicyComponent {
export class RestrictedItemTypesPolicyComponent extends BasePolicyEditComponent {
constructor() {
super();
}

View File

@@ -3,9 +3,10 @@ import { UntypedFormBuilder } from "@angular/forms";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
import { SharedModule } from "../../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
export class SendOptionsPolicy extends BasePolicy {
export class SendOptionsPolicy extends BasePolicyEditDefinition {
name = "sendOptions";
description = "sendOptionsPolicyDesc";
type = PolicyType.SendOptions;
@@ -13,11 +14,10 @@ export class SendOptionsPolicy extends BasePolicy {
}
@Component({
selector: "policy-send-options",
templateUrl: "send-options.component.html",
standalone: false,
imports: [SharedModule],
})
export class SendOptionsPolicyComponent extends BasePolicyComponent {
export class SendOptionsPolicyComponent extends BasePolicyEditComponent {
data = this.formBuilder.group({
disableHideEmail: false,
});

View File

@@ -2,9 +2,10 @@ import { Component, OnInit } from "@angular/core";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
import { SharedModule } from "../../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
export class SingleOrgPolicy extends BasePolicy {
export class SingleOrgPolicy extends BasePolicyEditDefinition {
name = "singleOrg";
description = "singleOrgPolicyDesc";
type = PolicyType.SingleOrg;
@@ -12,11 +13,10 @@ export class SingleOrgPolicy extends BasePolicy {
}
@Component({
selector: "policy-single-org",
templateUrl: "single-org.component.html",
standalone: false,
imports: [SharedModule],
})
export class SingleOrgPolicyComponent extends BasePolicyComponent implements OnInit {
export class SingleOrgPolicyComponent extends BasePolicyEditComponent implements OnInit {
async ngOnInit() {
super.ngOnInit();

View File

@@ -2,9 +2,10 @@ import { Component } from "@angular/core";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
import { SharedModule } from "../../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
export class TwoFactorAuthenticationPolicy extends BasePolicy {
export class TwoFactorAuthenticationPolicy extends BasePolicyEditDefinition {
name = "twoStepLoginPolicyTitle";
description = "twoStepLoginPolicyDesc";
type = PolicyType.TwoFactorAuthentication;
@@ -12,8 +13,7 @@ export class TwoFactorAuthenticationPolicy extends BasePolicy {
}
@Component({
selector: "policy-two-factor-authentication",
templateUrl: "two-factor-authentication.component.html",
standalone: false,
imports: [SharedModule],
})
export class TwoFactorAuthenticationPolicyComponent extends BasePolicyComponent {}
export class TwoFactorAuthenticationPolicyComponent extends BasePolicyEditComponent {}

View File

@@ -12,9 +12,8 @@ import { OrgKey } from "@bitwarden/common/types/key";
import { DialogService } from "@bitwarden/components";
import { EncString } from "@bitwarden/sdk-internal";
import { SharedModule } from "../../../shared";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
import { SharedModule } from "../../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
interface VNextPolicyRequest {
policy: PolicyRequest;
@@ -23,26 +22,24 @@ interface VNextPolicyRequest {
};
}
export class vNextOrganizationDataOwnershipPolicy extends BasePolicy {
export class vNextOrganizationDataOwnershipPolicy extends BasePolicyEditDefinition {
name = "organizationDataOwnership";
description = "organizationDataOwnershipDesc";
type = PolicyType.OrganizationDataOwnership;
component = vNextOrganizationDataOwnershipPolicyComponent;
showDescription = false;
override display(organization: Organization, configService: ConfigService): Observable<boolean> {
override display$(organization: Organization, configService: ConfigService): Observable<boolean> {
return configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation);
}
}
@Component({
selector: "vnext-policy-organization-data-ownership",
templateUrl: "vnext-organization-data-ownership.component.html",
standalone: true,
imports: [SharedModule],
})
export class vNextOrganizationDataOwnershipPolicyComponent
extends BasePolicyComponent
extends BasePolicyEditComponent
implements OnInit
{
constructor(
@@ -74,7 +71,7 @@ export class vNextOrganizationDataOwnershipPolicyComponent
const request: VNextPolicyRequest = {
policy: {
type: this.policy.type,
enabled: this.enabled.value,
enabled: this.enabled.value ?? false,
data: this.buildRequestData(),
},
metadata: {

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
AfterViewInit,
ChangeDetectorRef,
@@ -9,7 +7,7 @@ import {
ViewContainerRef,
} from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Observable, map, firstValueFrom, switchMap } from "rxjs";
import { Observable, map, firstValueFrom, switchMap, filter, of } from "rxjs";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -29,37 +27,38 @@ import {
} from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { BasePolicy, BasePolicyComponent } from "../policies";
import { vNextOrganizationDataOwnershipPolicyComponent } from "../policies/vnext-organization-data-ownership.component";
import { SharedModule } from "../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "./base-policy-edit.component";
import { vNextOrganizationDataOwnershipPolicyComponent } from "./policy-edit-definitions";
export type PolicyEditDialogData = {
/** Returns policy abstracts. */
policy: BasePolicy;
/** Returns a unique organization id */
/**
* The metadata containing information about how to display and edit the policy.
*/
policy: BasePolicyEditDefinition;
/**
* The organization ID for the policy.
*/
organizationId: string;
};
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum PolicyEditDialogResult {
Saved = "saved",
}
export type PolicyEditDialogResult = "saved";
@Component({
selector: "app-policy-edit",
templateUrl: "policy-edit.component.html",
standalone: false,
templateUrl: "policy-edit-dialog.component.html",
imports: [SharedModule],
})
export class PolicyEditComponent implements AfterViewInit {
export class PolicyEditDialogComponent implements AfterViewInit {
@ViewChild("policyForm", { read: ViewContainerRef, static: true })
policyFormRef: ViewContainerRef;
policyFormRef: ViewContainerRef | undefined;
policyType = PolicyType;
loading = true;
enabled = false;
saveDisabled$: Observable<boolean>;
policyComponent: BasePolicyComponent;
saveDisabled$: Observable<boolean> = of(false);
policyComponent: BasePolicyEditComponent | undefined;
private policyResponse: PolicyResponse;
formGroup = this.formBuilder.group({
enabled: [this.enabled],
});
@@ -75,35 +74,43 @@ export class PolicyEditComponent implements AfterViewInit {
private configService: ConfigService,
private keyService: KeyService,
) {}
get policy(): BasePolicy {
get policy(): BasePolicyEditDefinition {
return this.data.policy;
}
/**
* Instantiates the child policy component and inserts it into the view.
*/
async ngAfterViewInit() {
await this.load();
const policyResponse = await this.load();
this.loading = false;
this.policyComponent = this.policyFormRef.createComponent(this.data.policy.component)
.instance as BasePolicyComponent;
this.policyComponent.policy = this.data.policy;
this.policyComponent.policyResponse = this.policyResponse;
if (!this.policyFormRef) {
throw new Error("Template not initialized.");
}
this.saveDisabled$ = this.policyComponent.data.statusChanges.pipe(
map((status) => status !== "VALID" || !this.policyResponse.canToggleState),
);
this.policyComponent = this.policyFormRef.createComponent(this.data.policy.component).instance;
this.policyComponent.policy = this.data.policy;
this.policyComponent.policyResponse = policyResponse;
if (this.policyComponent.data) {
// If the policy has additional configuration, disable the save button if the form state is invalid
this.saveDisabled$ = this.policyComponent.data.statusChanges.pipe(
map((status) => status !== "VALID" || !policyResponse.canToggleState),
);
}
this.cdr.detectChanges();
}
async load() {
try {
this.policyResponse = await this.policyApiService.getPolicy(
this.data.organizationId,
this.data.policy.type,
);
} catch (e) {
return await this.policyApiService.getPolicy(this.data.organizationId, this.data.policy.type);
} catch (e: any) {
// No policy exists yet, instantiate an empty one
if (e.statusCode === 404) {
this.policyResponse = new PolicyResponse({ Enabled: false });
return new PolicyResponse({ Enabled: false });
} else {
throw e;
}
@@ -111,6 +118,10 @@ export class PolicyEditComponent implements AfterViewInit {
}
submit = async () => {
if (!this.policyComponent) {
throw new Error("PolicyComponent not initialized.");
}
if ((await this.policyComponent.confirm()) == false) {
this.dialogRef.close();
return;
@@ -128,14 +139,12 @@ export class PolicyEditComponent implements AfterViewInit {
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("editedPolicyId", this.i18nService.t(this.data.policy.name)),
});
this.dialogRef.close(PolicyEditDialogResult.Saved);
} catch (error) {
this.dialogRef.close("saved");
} catch (error: any) {
this.toastService.showToast({
variant: "error",
title: null,
message: error.message,
});
}
@@ -150,6 +159,10 @@ export class PolicyEditComponent implements AfterViewInit {
}
private async handleStandardSubmission(): Promise<void> {
if (!this.policyComponent) {
throw new Error("PolicyComponent not initialized.");
}
const request = await this.policyComponent.buildRequest();
await this.policyApiService.putPolicy(this.data.organizationId, this.data.policy.type, request);
}
@@ -161,10 +174,8 @@ export class PolicyEditComponent implements AfterViewInit {
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.keyService.orgKeys$(userId)),
map(
(orgKeys: { [key: OrganizationId]: any }) =>
orgKeys[this.data.organizationId as OrganizationId] ?? null,
),
filter((orgKeys) => orgKeys != null),
map((orgKeys) => orgKeys[this.data.organizationId as OrganizationId] ?? null),
),
);
@@ -181,6 +192,6 @@ export class PolicyEditComponent implements AfterViewInit {
);
}
static open = (dialogService: DialogService, config: DialogConfig<PolicyEditDialogData>) => {
return dialogService.open<PolicyEditDialogResult>(PolicyEditComponent, config);
return dialogService.open<PolicyEditDialogResult>(PolicyEditDialogComponent, config);
};
}

View File

@@ -0,0 +1,34 @@
import { BasePolicyEditDefinition } from "./base-policy-edit.component";
import {
DisableSendPolicy,
MasterPasswordPolicy,
OrganizationDataOwnershipPolicy,
PasswordGeneratorPolicy,
RemoveUnlockWithPinPolicy,
RequireSsoPolicy,
ResetPasswordPolicy,
RestrictedItemTypesPolicy,
SendOptionsPolicy,
SingleOrgPolicy,
TwoFactorAuthenticationPolicy,
vNextOrganizationDataOwnershipPolicy,
} from "./policy-edit-definitions";
/**
* The policy register for OSS policies.
* Add your policy definition here if it is under the OSS license.
*/
export const ossPolicyEditRegister: BasePolicyEditDefinition[] = [
new TwoFactorAuthenticationPolicy(),
new MasterPasswordPolicy(),
new RemoveUnlockWithPinPolicy(),
new ResetPasswordPolicy(),
new PasswordGeneratorPolicy(),
new SingleOrgPolicy(),
new RequireSsoPolicy(),
new OrganizationDataOwnershipPolicy(),
new vNextOrganizationDataOwnershipPolicy(),
new DisableSendPolicy(),
new SendOptionsPolicy(),
new RestrictedItemTypesPolicy(),
];

View File

@@ -0,0 +1,13 @@
import { BasePolicyEditDefinition } from "./base-policy-edit.component";
export class PolicyListService {
private policies: readonly BasePolicyEditDefinition[];
constructor(policies: BasePolicyEditDefinition[]) {
this.policies = Object.freeze([...policies]);
}
getPolicies() {
return this.policies;
}
}

View File

@@ -0,0 +1,7 @@
import { SafeInjectionToken } from "@bitwarden/ui-common";
import { BasePolicyEditDefinition } from "./base-policy-edit.component";
export const POLICY_EDIT_REGISTER = new SafeInjectionToken<BasePolicyEditDefinition[]>(
"POLICY_EDIT_REGISTER",
);

View File

@@ -7,7 +7,6 @@ import { DangerZoneComponent } from "../../../auth/settings/account/danger-zone.
import { HeaderModule } from "../../../layouts/header/header.module";
import { SharedModule } from "../../../shared";
import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component";
import { PoliciesModule } from "../../organizations/policies";
import { AccountComponent } from "./account.component";
import { OrganizationSettingsRoutingModule } from "./organization-settings-routing.module";
@@ -16,7 +15,6 @@ import { TwoFactorSetupComponent } from "./two-factor-setup.component";
@NgModule({
imports: [
SharedModule,
PoliciesModule,
OrganizationSettingsRoutingModule,
AccountFingerprintComponent,
DangerZoneComponent,

View File

@@ -30,22 +30,6 @@ import { SearchService } from "@bitwarden/common/vault/abstractions/search.servi
import { DialogService, ToastService } from "@bitwarden/components";
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
import { PolicyListService } from "./admin-console/core/policy-list.service";
import {
DisableSendPolicy,
MasterPasswordPolicy,
PasswordGeneratorPolicy,
OrganizationDataOwnershipPolicy,
vNextOrganizationDataOwnershipPolicy,
RequireSsoPolicy,
ResetPasswordPolicy,
SendOptionsPolicy,
SingleOrgPolicy,
TwoFactorAuthenticationPolicy,
RemoveUnlockWithPinPolicy,
RestrictedItemTypesPolicy,
} from "./admin-console/organizations/policies";
const BroadcasterSubscriptionId = "AppComponent";
const IdleTimeout = 60000 * 10; // 10 minutes
@@ -79,7 +63,6 @@ export class AppComponent implements OnDestroy, OnInit {
private serverNotificationsService: ServerNotificationsService,
private stateService: StateService,
private eventUploadService: EventUploadService,
protected policyListService: PolicyListService,
protected configService: ConfigService,
private dialogService: DialogService,
private biometricStateService: BiometricStateService,
@@ -238,21 +221,6 @@ export class AppComponent implements OnDestroy, OnInit {
}
});
});
this.policyListService.addPolicies([
new TwoFactorAuthenticationPolicy(),
new MasterPasswordPolicy(),
new RemoveUnlockWithPinPolicy(),
new ResetPasswordPolicy(),
new PasswordGeneratorPolicy(),
new SingleOrgPolicy(),
new RequireSsoPolicy(),
new OrganizationDataOwnershipPolicy(),
new vNextOrganizationDataOwnershipPolicy(),
new DisableSendPolicy(),
new SendOptionsPolicy(),
new RestrictedItemTypesPolicy(),
]);
}
ngOnDestroy() {

View File

@@ -113,7 +113,10 @@ import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarde
import { WebOrganizationInviteService } from "@bitwarden/web-vault/app/auth/core/services/organization-invite/web-organization-invite.service";
import { flagEnabled } from "../../utils/flags";
import { PolicyListService } from "../admin-console/core/policy-list.service";
import {
POLICY_EDIT_REGISTER,
ossPolicyEditRegister,
} from "../admin-console/organizations/policies";
import {
WebChangePasswordService,
WebRegistrationFinishService,
@@ -152,7 +155,10 @@ const safeProviders: SafeProvider[] = [
safeProvider(InitService),
safeProvider(RouterService),
safeProvider(EventService),
safeProvider(PolicyListService),
safeProvider({
provide: POLICY_EDIT_REGISTER,
useValue: ossPolicyEditRegister,
}),
safeProvider({
provide: DEFAULT_VAULT_TIMEOUT,
deps: [PlatformUtilsService],