1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-28 12:45:45 +01:00

[PM-13449] Owner assignment/visibility in AC (#11588)

* Revert "remove logic for personal ownership, not needed in AC"

This reverts commit f04fef59f4.

* allow for ownership to be controlled from the admin console when cloning a cipher
This commit is contained in:
Nick Krantz 2024-11-01 14:15:36 -05:00 committed by GitHub
parent b0a73cfe45
commit f416c3ed49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 119 additions and 14 deletions

View File

@ -4,6 +4,8 @@ import { BehaviorSubject } from "rxjs";
import { CollectionAdminService } from "@bitwarden/admin-console/common"; import { CollectionAdminService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; 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 { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherId } from "@bitwarden/common/types/guid"; import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -16,8 +18,25 @@ describe("AdminConsoleCipherFormConfigService", () => {
let adminConsoleConfigService: AdminConsoleCipherFormConfigService; let adminConsoleConfigService: AdminConsoleCipherFormConfigService;
const cipherId = "333-444-555" as CipherId; const cipherId = "333-444-555" as CipherId;
const testOrg = { id: "333-44-55", name: "Test Org", canEditAllCiphers: false }; const testOrg = {
id: "333-44-55",
name: "Test Org",
canEditAllCiphers: false,
isMember: true,
enabled: true,
status: OrganizationUserStatusType.Confirmed,
};
const testOrg2 = {
id: "333-999-888",
name: "Test Org 2",
canEditAllCiphers: false,
isMember: true,
enabled: true,
status: OrganizationUserStatusType.Confirmed,
};
const policyAppliesToActiveUser$ = new BehaviorSubject<boolean>(true);
const organization$ = new BehaviorSubject<Organization>(testOrg as Organization); const organization$ = new BehaviorSubject<Organization>(testOrg as Organization);
const organizations$ = new BehaviorSubject<Organization[]>([testOrg, testOrg2] as Organization[]);
const getCipherAdmin = jest.fn().mockResolvedValue(null); const getCipherAdmin = jest.fn().mockResolvedValue(null);
const getCipher = jest.fn().mockResolvedValue(null); const getCipher = jest.fn().mockResolvedValue(null);
@ -30,7 +49,11 @@ describe("AdminConsoleCipherFormConfigService", () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
providers: [ providers: [
AdminConsoleCipherFormConfigService, AdminConsoleCipherFormConfigService,
{ provide: OrganizationService, useValue: { get$: () => organization$ } }, {
provide: PolicyService,
useValue: { policyAppliesToActiveUser$: () => policyAppliesToActiveUser$ },
},
{ provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } },
{ provide: CipherService, useValue: { get: getCipher } }, { provide: CipherService, useValue: { get: getCipher } },
{ provide: CollectionAdminService, useValue: { getAll: () => Promise.resolve([]) } }, { provide: CollectionAdminService, useValue: { getAll: () => Promise.resolve([]) } },
{ {
@ -79,12 +102,55 @@ describe("AdminConsoleCipherFormConfigService", () => {
expect(result.admin).toBe(true); expect(result.admin).toBe(true);
}); });
it("sets `allowPersonalOwnership` to false", async () => { it("sets `allowPersonalOwnership`", async () => {
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
const result = await adminConsoleConfigService.buildConfig("clone", cipherId); policyAppliesToActiveUser$.next(true);
let result = await adminConsoleConfigService.buildConfig("clone", cipherId);
expect(result.allowPersonalOwnership).toBe(false); expect(result.allowPersonalOwnership).toBe(false);
policyAppliesToActiveUser$.next(false);
result = await adminConsoleConfigService.buildConfig("clone", cipherId);
expect(result.allowPersonalOwnership).toBe(true);
});
it("disables personal ownership when not cloning", async () => {
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
policyAppliesToActiveUser$.next(false);
let result = await adminConsoleConfigService.buildConfig("add", cipherId);
expect(result.allowPersonalOwnership).toBe(false);
result = await adminConsoleConfigService.buildConfig("edit", cipherId);
expect(result.allowPersonalOwnership).toBe(false);
result = await adminConsoleConfigService.buildConfig("clone", cipherId);
expect(result.allowPersonalOwnership).toBe(true);
});
it("returns all ciphers when cloning a cipher", async () => {
// Add cipher
let result = await adminConsoleConfigService.buildConfig("add", cipherId);
expect(result.organizations).toEqual([testOrg]);
// Edit cipher
result = await adminConsoleConfigService.buildConfig("edit", cipherId);
expect(result.organizations).toEqual([testOrg]);
// Clone cipher
result = await adminConsoleConfigService.buildConfig("clone", cipherId);
expect(result.organizations).toEqual([testOrg, testOrg2]);
}); });
describe("getCipher", () => { describe("getCipher", () => {

View File

@ -4,6 +4,8 @@ import { combineLatest, filter, firstValueFrom, map, switchMap } from "rxjs";
import { CollectionAdminService } from "@bitwarden/admin-console/common"; import { CollectionAdminService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; 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, OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherId } from "@bitwarden/common/types/guid"; import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -21,23 +23,40 @@ import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/se
/** Admin Console implementation of the `CipherFormConfigService`. */ /** Admin Console implementation of the `CipherFormConfigService`. */
@Injectable() @Injectable()
export class AdminConsoleCipherFormConfigService implements CipherFormConfigService { export class AdminConsoleCipherFormConfigService implements CipherFormConfigService {
private policyService: PolicyService = inject(PolicyService);
private organizationService: OrganizationService = inject(OrganizationService); private organizationService: OrganizationService = inject(OrganizationService);
private cipherService: CipherService = inject(CipherService); private cipherService: CipherService = inject(CipherService);
private routedVaultFilterService: RoutedVaultFilterService = inject(RoutedVaultFilterService); private routedVaultFilterService: RoutedVaultFilterService = inject(RoutedVaultFilterService);
private collectionAdminService: CollectionAdminService = inject(CollectionAdminService); private collectionAdminService: CollectionAdminService = inject(CollectionAdminService);
private apiService: ApiService = inject(ApiService); private apiService: ApiService = inject(ApiService);
private allowPersonalOwnership$ = this.policyService
.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
.pipe(map((p) => !p));
private organizationId$ = this.routedVaultFilterService.filter$.pipe( private organizationId$ = this.routedVaultFilterService.filter$.pipe(
map((filter) => filter.organizationId), map((filter) => filter.organizationId),
filter((filter) => filter !== undefined), filter((filter) => filter !== undefined),
); );
private organization$ = this.organizationId$.pipe( private allOrganizations$ = this.organizationService.organizations$.pipe(
switchMap((organizationId) => this.organizationService.get$(organizationId)), map((orgs) => {
return orgs.filter(
(o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed,
);
}),
);
private organization$ = combineLatest([this.allOrganizations$, this.organizationId$]).pipe(
map(([orgs, orgId]) => orgs.find((o) => o.id === orgId)),
); );
private editableCollections$ = this.organization$.pipe( private editableCollections$ = this.organization$.pipe(
switchMap(async (org) => { switchMap(async (org) => {
if (!org) {
return [];
}
const collections = await this.collectionAdminService.getAll(org.id); const collections = await this.collectionAdminService.getAll(org.id);
// Users that can edit all ciphers can implicitly add to / edit within any collection // Users that can edit all ciphers can implicitly add to / edit within any collection
if (org.canEditAllCiphers) { if (org.canEditAllCiphers) {
@ -53,26 +72,39 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
cipherId?: CipherId, cipherId?: CipherId,
cipherType?: CipherType, cipherType?: CipherType,
): Promise<CipherFormConfig> { ): Promise<CipherFormConfig> {
const [organization, allCollections] = await firstValueFrom( const [organization, allowPersonalOwnership, allOrganizations, allCollections] =
combineLatest([this.organization$, this.editableCollections$]), await firstValueFrom(
); combineLatest([
this.organization$,
this.allowPersonalOwnership$,
this.allOrganizations$,
this.editableCollections$,
]),
);
const cipher = await this.getCipher(organization, cipherId); const cipher = await this.getCipher(organization, cipherId);
const collections = allCollections.filter( const collections = allCollections.filter(
(c) => c.organizationId === organization.id && c.assigned && !c.readOnly, (c) => c.organizationId === organization.id && c.assigned && !c.readOnly,
); );
// When cloning from within the Admin Console, all organizations should be available.
// Otherwise only the one in context should be
const organizations = mode === "clone" ? allOrganizations : [organization];
// Only allow the user to assign to their personal vault when cloning and
// the policies are enabled for it.
const allowPersonalOwnershipOnlyForClone = mode === "clone" ? allowPersonalOwnership : false;
return { return {
mode, mode,
cipherType: cipher?.type ?? cipherType ?? CipherType.Login, cipherType: cipher?.type ?? cipherType ?? CipherType.Login,
admin: organization.canEditAllCiphers ?? false, admin: organization.canEditAllCiphers ?? false,
allowPersonalOwnership: false, allowPersonalOwnership: allowPersonalOwnershipOnlyForClone,
originalCipher: cipher, originalCipher: cipher,
collections, collections,
organizations: [organization], // only a single org is in context at a time organizations,
folders: [], // folders not applicable in the admin console folders: [], // folders not applicable in the admin console
hideIndividualVaultFields: true, hideIndividualVaultFields: true,
isAdminConsole: true,
}; };
} }

View File

@ -82,6 +82,9 @@ type BaseCipherFormConfig = {
/** Hides the fields that are only applicable to individuals, useful in the Admin Console where folders aren't applicable */ /** Hides the fields that are only applicable to individuals, useful in the Admin Console where folders aren't applicable */
hideIndividualVaultFields?: true; hideIndividualVaultFields?: true;
/** True when the config is built within the context of the Admin Console */
isAdminConsole?: true;
}; };
/** /**

View File

@ -163,9 +163,13 @@ export class ItemDetailsSectionComponent implements OnInit {
} }
get showOwnership() { get showOwnership() {
return ( // Show ownership field when editing with available orgs
this.allowOwnershipChange || (this.organizations.length > 0 && this.config.mode === "edit") const isEditingWithOrgs = this.organizations.length > 0 && this.config.mode === "edit";
);
// When in admin console, ownership should not be shown unless cloning
const isAdminConsoleEdit = this.config.isAdminConsole && this.config.mode !== "clone";
return this.allowOwnershipChange || (isEditingWithOrgs && !isAdminConsoleEdit);
} }
get defaultOwner() { get defaultOwner() {