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:
parent
b0a73cfe45
commit
f416c3ed49
@ -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", () => {
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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() {
|
||||||
|
Loading…
Reference in New Issue
Block a user