mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[PM-20642] - [Vault] [Web App] Front End Changes to Enforce "Remove card item type policy" (#15097)
* add restricted item types service and apply it to filter web cipher * code cleanup. add shareReplay * account for multiple orgs when restricting item types * restrict item types for specific orgs * clean up logic. use policiesByType$ * track by item.type * clean up filtering. prefer observable. do not exempt owners for restricted item types * simplify in vault-filter. move item filter logic to vault. fix tests * don't return early in filter-function
This commit is contained in:
@@ -16,5 +16,5 @@ export enum PolicyType {
|
||||
AutomaticAppLogIn = 12, // Enables automatic log in of apps from configured identity provider
|
||||
FreeFamiliesSponsorshipPolicy = 13, // Disables free families plan for organization
|
||||
RemoveUnlockWithPin = 14, // Do not allow members to unlock their account with a PIN.
|
||||
RestrictedItemTypesPolicy = 15, // Restricts item types that can be created within an organization
|
||||
RestrictedItemTypes = 15, // Restricts item types that can be created within an organization
|
||||
}
|
||||
|
||||
@@ -228,15 +228,19 @@ export class DefaultPolicyService implements PolicyService {
|
||||
case PolicyType.MaximumVaultTimeout:
|
||||
// Max Vault Timeout applies to everyone except owners
|
||||
return organization.isOwner;
|
||||
// the following policies apply to everyone
|
||||
case PolicyType.PasswordGenerator:
|
||||
// password generation policy applies to everyone
|
||||
// password generation policy
|
||||
return false;
|
||||
case PolicyType.FreeFamiliesSponsorshipPolicy:
|
||||
// free Bitwarden families policy
|
||||
return false;
|
||||
case PolicyType.RestrictedItemTypes:
|
||||
// restricted item types policy
|
||||
return false;
|
||||
case PolicyType.PersonalOwnership:
|
||||
// individual vault policy applies to everyone except admins and owners
|
||||
return organization.isAdmin;
|
||||
case PolicyType.FreeFamiliesSponsorshipPolicy:
|
||||
// free Bitwarden families policy applies to everyone
|
||||
return false;
|
||||
default:
|
||||
return organization.canManagePolicies;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ export * as VaultIcons from "./icons";
|
||||
|
||||
export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service";
|
||||
export { SshImportPromptService } from "./services/ssh-import-prompt.service";
|
||||
export {
|
||||
RestrictedItemTypesService,
|
||||
RestrictedCipherType,
|
||||
} from "./services/restricted-item-types.service";
|
||||
|
||||
export * from "./abstractions/change-login-password.service";
|
||||
export * from "./services/default-change-login-password.service";
|
||||
|
||||
137
libs/vault/src/services/restricted-item-types.service.spec.ts
Normal file
137
libs/vault/src/services/restricted-item-types.service.spec.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { RestrictedItemTypesService, RestrictedCipherType } from "./restricted-item-types.service";
|
||||
|
||||
describe("RestrictedItemTypesService", () => {
|
||||
let service: RestrictedItemTypesService;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let fakeAccount: Account | null;
|
||||
|
||||
const org1: Organization = { id: "org1" } as any;
|
||||
const org2: Organization = { id: "org2" } as any;
|
||||
|
||||
const policyOrg1 = {
|
||||
organizationId: "org1",
|
||||
type: PolicyType.RestrictedItemTypes,
|
||||
enabled: true,
|
||||
data: [CipherType.Card],
|
||||
} as Policy;
|
||||
|
||||
const policyOrg2 = {
|
||||
organizationId: "org2",
|
||||
type: PolicyType.RestrictedItemTypes,
|
||||
enabled: true,
|
||||
data: [CipherType.Card],
|
||||
} as Policy;
|
||||
|
||||
beforeEach(() => {
|
||||
policyService = mock<PolicyService>();
|
||||
organizationService = mock<OrganizationService>();
|
||||
accountService = mock<AccountService>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
fakeAccount = { id: Utils.newGuid() as UserId } as Account;
|
||||
accountService.activeAccount$ = of(fakeAccount);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: PolicyService, useValue: policyService },
|
||||
{ provide: OrganizationService, useValue: organizationService },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
],
|
||||
});
|
||||
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
organizationService.organizations$.mockReturnValue(of([org1, org2]));
|
||||
policyService.policiesByType$.mockReturnValue(of([]));
|
||||
service = TestBed.inject(RestrictedItemTypesService);
|
||||
});
|
||||
|
||||
it("emits empty array when feature flag is disabled", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
|
||||
const result = await firstValueFrom(service.restricted$);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("emits empty array if no organizations exist", async () => {
|
||||
organizationService.organizations$.mockReturnValue(of([]));
|
||||
policyService.policiesByType$.mockReturnValue(of([]));
|
||||
|
||||
const result = await firstValueFrom(service.restricted$);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("defaults undefined data to [Card] and returns empty allowViewOrgIds", async () => {
|
||||
organizationService.organizations$.mockReturnValue(of([org1]));
|
||||
|
||||
const policyForOrg1 = {
|
||||
organizationId: "org1",
|
||||
type: PolicyType.RestrictedItemTypes,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
} as Policy;
|
||||
policyService.policiesByType$.mockReturnValue(of([policyForOrg1]));
|
||||
|
||||
const result = await firstValueFrom(service.restricted$);
|
||||
expect(result).toEqual<RestrictedCipherType[]>([
|
||||
{ cipherType: CipherType.Card, allowViewOrgIds: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("if one org restricts Card and another has no policy, allowViewOrgIds contains the unrestricted org", async () => {
|
||||
policyService.policiesByType$.mockReturnValue(of([policyOrg1]));
|
||||
|
||||
const result = await firstValueFrom(service.restricted$);
|
||||
expect(result).toEqual<RestrictedCipherType[]>([
|
||||
{ cipherType: CipherType.Card, allowViewOrgIds: ["org2"] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns empty allowViewOrgIds when all orgs restrict the same type", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
organizationService.organizations$.mockReturnValue(of([org1, org2]));
|
||||
policyService.policiesByType$.mockReturnValue(of([policyOrg1, policyOrg2]));
|
||||
|
||||
const result = await firstValueFrom(service.restricted$);
|
||||
expect(result).toEqual<RestrictedCipherType[]>([
|
||||
{ cipherType: CipherType.Card, allowViewOrgIds: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("aggregates multiple types and computes allowViewOrgIds correctly", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
organizationService.organizations$.mockReturnValue(of([org1, org2]));
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([
|
||||
{ ...policyOrg1, data: [CipherType.Card, CipherType.Login] } as Policy,
|
||||
{ ...policyOrg2, data: [CipherType.Card, CipherType.Identity] } as Policy,
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(service.restricted$);
|
||||
|
||||
expect(result).toEqual<RestrictedCipherType[]>([
|
||||
{ cipherType: CipherType.Card, allowViewOrgIds: [] },
|
||||
{ cipherType: CipherType.Login, allowViewOrgIds: ["org2"] },
|
||||
{ cipherType: CipherType.Identity, allowViewOrgIds: ["org1"] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
80
libs/vault/src/services/restricted-item-types.service.ts
Normal file
80
libs/vault/src/services/restricted-item-types.service.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { combineLatest, map, of, Observable } from "rxjs";
|
||||
import { switchMap, distinctUntilChanged, shareReplay } from "rxjs/operators";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
export type RestrictedCipherType = {
|
||||
cipherType: CipherType;
|
||||
allowViewOrgIds: string[];
|
||||
};
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class RestrictedItemTypesService {
|
||||
/**
|
||||
* Emits an array of RestrictedCipherType objects:
|
||||
* - cipherType: each type restricted by at least one org-level policy
|
||||
* - allowViewOrgIds: org IDs that allow viewing that type
|
||||
*/
|
||||
readonly restricted$: Observable<RestrictedCipherType[]> = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.RemoveCardItemTypePolicy)
|
||||
.pipe(
|
||||
switchMap((flagOn) => {
|
||||
if (!flagOn) {
|
||||
return of([]);
|
||||
}
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
combineLatest([
|
||||
this.organizationService.organizations$(userId),
|
||||
this.policyService.policiesByType$(PolicyType.RestrictedItemTypes, userId),
|
||||
]),
|
||||
),
|
||||
map(([orgs, enabledPolicies]) => {
|
||||
// Helper to extract restricted types, defaulting to [Card]
|
||||
const restrictedTypes = (p: (typeof enabledPolicies)[number]) =>
|
||||
(p.data as CipherType[]) ?? [CipherType.Card];
|
||||
|
||||
// Union across all enabled policies
|
||||
const allRestrictedTypes = Array.from(
|
||||
new Set(enabledPolicies.flatMap(restrictedTypes)),
|
||||
);
|
||||
|
||||
return allRestrictedTypes.map((cipherType) => {
|
||||
// Determine which orgs allow viewing this type
|
||||
const allowViewOrgIds = orgs
|
||||
.filter((org) => {
|
||||
const orgPolicy = enabledPolicies.find((p) => p.organizationId === org.id);
|
||||
// no policy for this org => allows everything
|
||||
if (!orgPolicy) {
|
||||
return true;
|
||||
}
|
||||
// if this type not in their restricted list => they allow it
|
||||
return !restrictedTypes(orgPolicy).includes(cipherType);
|
||||
})
|
||||
.map((org) => org.id);
|
||||
|
||||
return { cipherType, allowViewOrgIds };
|
||||
});
|
||||
}),
|
||||
);
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
private organizationService: OrganizationService,
|
||||
private policyService: PolicyService,
|
||||
) {}
|
||||
}
|
||||
Reference in New Issue
Block a user