1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-25 12:15:18 +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 { ApiService } from "@bitwarden/common/abstractions/api.service";
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 { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -16,8 +18,25 @@ describe("AdminConsoleCipherFormConfigService", () => {
let adminConsoleConfigService: AdminConsoleCipherFormConfigService;
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 organizations$ = new BehaviorSubject<Organization[]>([testOrg, testOrg2] as Organization[]);
const getCipherAdmin = jest.fn().mockResolvedValue(null);
const getCipher = jest.fn().mockResolvedValue(null);
@ -30,7 +49,11 @@ describe("AdminConsoleCipherFormConfigService", () => {
await TestBed.configureTestingModule({
providers: [
AdminConsoleCipherFormConfigService,
{ provide: OrganizationService, useValue: { get$: () => organization$ } },
{
provide: PolicyService,
useValue: { policyAppliesToActiveUser$: () => policyAppliesToActiveUser$ },
},
{ provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } },
{ provide: CipherService, useValue: { get: getCipher } },
{ provide: CollectionAdminService, useValue: { getAll: () => Promise.resolve([]) } },
{
@ -79,12 +102,55 @@ describe("AdminConsoleCipherFormConfigService", () => {
expect(result.admin).toBe(true);
});
it("sets `allowPersonalOwnership` to false", async () => {
it("sets `allowPersonalOwnership`", async () => {
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);
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", () => {

View File

@ -4,6 +4,8 @@ import { combineLatest, filter, firstValueFrom, map, switchMap } from "rxjs";
import { CollectionAdminService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
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 { CipherId } from "@bitwarden/common/types/guid";
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`. */
@Injectable()
export class AdminConsoleCipherFormConfigService implements CipherFormConfigService {
private policyService: PolicyService = inject(PolicyService);
private organizationService: OrganizationService = inject(OrganizationService);
private cipherService: CipherService = inject(CipherService);
private routedVaultFilterService: RoutedVaultFilterService = inject(RoutedVaultFilterService);
private collectionAdminService: CollectionAdminService = inject(CollectionAdminService);
private apiService: ApiService = inject(ApiService);
private allowPersonalOwnership$ = this.policyService
.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
.pipe(map((p) => !p));
private organizationId$ = this.routedVaultFilterService.filter$.pipe(
map((filter) => filter.organizationId),
filter((filter) => filter !== undefined),
);
private organization$ = this.organizationId$.pipe(
switchMap((organizationId) => this.organizationService.get$(organizationId)),
private allOrganizations$ = this.organizationService.organizations$.pipe(
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(
switchMap(async (org) => {
if (!org) {
return [];
}
const collections = await this.collectionAdminService.getAll(org.id);
// Users that can edit all ciphers can implicitly add to / edit within any collection
if (org.canEditAllCiphers) {
@ -53,26 +72,39 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
cipherId?: CipherId,
cipherType?: CipherType,
): Promise<CipherFormConfig> {
const [organization, allCollections] = await firstValueFrom(
combineLatest([this.organization$, this.editableCollections$]),
);
const [organization, allowPersonalOwnership, allOrganizations, allCollections] =
await firstValueFrom(
combineLatest([
this.organization$,
this.allowPersonalOwnership$,
this.allOrganizations$,
this.editableCollections$,
]),
);
const cipher = await this.getCipher(organization, cipherId);
const collections = allCollections.filter(
(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 {
mode,
cipherType: cipher?.type ?? cipherType ?? CipherType.Login,
admin: organization.canEditAllCiphers ?? false,
allowPersonalOwnership: false,
allowPersonalOwnership: allowPersonalOwnershipOnlyForClone,
originalCipher: cipher,
collections,
organizations: [organization], // only a single org is in context at a time
organizations,
folders: [], // folders not applicable in the admin console
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 */
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() {
return (
this.allowOwnershipChange || (this.organizations.length > 0 && this.config.mode === "edit")
);
// Show ownership field when editing with available orgs
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() {