-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[PM-10426] Admin Console - Edit Modal (#11249)
* add `hideFolderSelection` for admin console ciphers * hide folder form field when configuration has `hideFolderSelection` set to true * add `addCipherV2` method in the admin console vault * add browser refresh logic for add/edit form * add admin console implementation of `AdminConsoleCipherFormConfigService` * only allow edit dialog in admin console * remove duplicate check * refactor comments * initial integration of combined dialog * integrate add cipher with admin console vault * account for special admin console collection permissions * add `edit` variable to AC ciphers when the user has permissions * Move comment to JSDoc * pass full cipher to view component * validate edit access when opening view form * partial-edit not applicable for admin console * refactor hideIndividualFields to be more generic and hide favorite button * pass entire cipher into edit logic to match view logic * add null check for cipher when attempting to view * remove logic for personal ownership, not needed in AC
- Loading branch information
1 parent
7098a24
commit a6db7e3
Showing
5 changed files
with
361 additions
and
54 deletions.
There are no files selected for viewing
119 changes: 119 additions & 0 deletions
119
apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { TestBed } from "@angular/core/testing"; | ||
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 { 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"; | ||
|
||
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; | ||
|
||
import { AdminConsoleCipherFormConfigService } from "./admin-console-cipher-form-config.service"; | ||
|
||
describe("AdminConsoleCipherFormConfigService", () => { | ||
let adminConsoleConfigService: AdminConsoleCipherFormConfigService; | ||
|
||
const cipherId = "333-444-555" as CipherId; | ||
const testOrg = { id: "333-44-55", name: "Test Org", canEditAllCiphers: false }; | ||
const organization$ = new BehaviorSubject<Organization>(testOrg as Organization); | ||
const getCipherAdmin = jest.fn().mockResolvedValue(null); | ||
const getCipher = jest.fn().mockResolvedValue(null); | ||
|
||
beforeEach(async () => { | ||
getCipherAdmin.mockClear(); | ||
getCipher.mockClear(); | ||
getCipher.mockResolvedValue({ id: cipherId, name: "Test Cipher - (non-admin)" }); | ||
getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); | ||
|
||
await TestBed.configureTestingModule({ | ||
providers: [ | ||
AdminConsoleCipherFormConfigService, | ||
{ provide: OrganizationService, useValue: { get$: () => organization$ } }, | ||
{ provide: CipherService, useValue: { get: getCipher } }, | ||
{ provide: CollectionAdminService, useValue: { getAll: () => Promise.resolve([]) } }, | ||
{ | ||
provide: RoutedVaultFilterService, | ||
useValue: { filter$: new BehaviorSubject({ organizationId: testOrg.id }) }, | ||
}, | ||
{ provide: ApiService, useValue: { getCipherAdmin } }, | ||
], | ||
}); | ||
}); | ||
|
||
describe("buildConfig", () => { | ||
it("sets individual attributes", async () => { | ||
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); | ||
|
||
const { folders, hideIndividualVaultFields } = await adminConsoleConfigService.buildConfig( | ||
"add", | ||
cipherId, | ||
); | ||
|
||
expect(folders).toEqual([]); | ||
expect(hideIndividualVaultFields).toBe(true); | ||
}); | ||
|
||
it("sets mode based on passed mode", async () => { | ||
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); | ||
|
||
const { mode } = await adminConsoleConfigService.buildConfig("edit", cipherId); | ||
|
||
expect(mode).toBe("edit"); | ||
}); | ||
|
||
it("sets admin flag based on `canEditAllCiphers`", async () => { | ||
// Disable edit all ciphers on org | ||
testOrg.canEditAllCiphers = false; | ||
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); | ||
|
||
let result = await adminConsoleConfigService.buildConfig("add", cipherId); | ||
|
||
expect(result.admin).toBe(false); | ||
|
||
// Enable edit all ciphers on org | ||
testOrg.canEditAllCiphers = true; | ||
result = await adminConsoleConfigService.buildConfig("add", cipherId); | ||
|
||
expect(result.admin).toBe(true); | ||
}); | ||
|
||
it("sets `allowPersonalOwnership` to false", async () => { | ||
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); | ||
|
||
const result = await adminConsoleConfigService.buildConfig("clone", cipherId); | ||
|
||
expect(result.allowPersonalOwnership).toBe(false); | ||
}); | ||
|
||
describe("getCipher", () => { | ||
it("retrieves the cipher from the cipher service", async () => { | ||
testOrg.canEditAllCiphers = false; | ||
|
||
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); | ||
|
||
const result = await adminConsoleConfigService.buildConfig("clone", cipherId); | ||
|
||
expect(getCipher).toHaveBeenCalledWith(cipherId); | ||
expect(result.originalCipher.name).toBe("Test Cipher - (non-admin)"); | ||
|
||
// Admin service not needed when cipher service can return the cipher | ||
expect(getCipherAdmin).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("retrieves the cipher from the admin service", async () => { | ||
getCipher.mockResolvedValueOnce(null); | ||
getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); | ||
|
||
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); | ||
|
||
await adminConsoleConfigService.buildConfig("add", cipherId); | ||
|
||
expect(getCipherAdmin).toHaveBeenCalledWith(cipherId); | ||
|
||
expect(getCipher).toHaveBeenCalledWith(cipherId); | ||
}); | ||
}); | ||
}); | ||
}); |
99 changes: 99 additions & 0 deletions
99
apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { inject, Injectable } from "@angular/core"; | ||
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 { 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"; | ||
import { CipherType } from "@bitwarden/common/vault/enums"; | ||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; | ||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; | ||
|
||
import { | ||
CipherFormConfig, | ||
CipherFormConfigService, | ||
CipherFormMode, | ||
} from "../../../../../../../libs/vault/src/cipher-form/abstractions/cipher-form-config.service"; | ||
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; | ||
|
||
/** Admin Console implementation of the `CipherFormConfigService`. */ | ||
@Injectable() | ||
export class AdminConsoleCipherFormConfigService implements CipherFormConfigService { | ||
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 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 editableCollections$ = this.organization$.pipe( | ||
switchMap(async (org) => { | ||
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) { | ||
return collections; | ||
} | ||
// The user is only allowed to add/edit items to assigned collections that are not readonly | ||
return collections.filter((c) => c.assigned && !c.readOnly); | ||
}), | ||
); | ||
|
||
async buildConfig( | ||
mode: CipherFormMode, | ||
cipherId?: CipherId, | ||
cipherType?: CipherType, | ||
): Promise<CipherFormConfig> { | ||
const [organization, allCollections] = await firstValueFrom( | ||
combineLatest([this.organization$, this.editableCollections$]), | ||
); | ||
|
||
const cipher = await this.getCipher(organization, cipherId); | ||
|
||
const collections = allCollections.filter( | ||
(c) => c.organizationId === organization.id && c.assigned && !c.readOnly, | ||
); | ||
|
||
return { | ||
mode, | ||
cipherType: cipher?.type ?? cipherType ?? CipherType.Login, | ||
admin: organization.canEditAllCiphers ?? false, | ||
allowPersonalOwnership: false, | ||
originalCipher: cipher, | ||
collections, | ||
organizations: [organization], // only a single org is in context at a time | ||
folders: [], // folders not applicable in the admin console | ||
hideIndividualVaultFields: true, | ||
}; | ||
} | ||
|
||
private async getCipher(organization: Organization, id?: CipherId): Promise<Cipher | null> { | ||
if (id == null) { | ||
return Promise.resolve(null); | ||
} | ||
|
||
// Check to see if the user has direct access to the cipher | ||
const cipherFromCipherService = await this.cipherService.get(id); | ||
|
||
// If the organization doesn't allow admin/owners to edit all ciphers return the cipher | ||
if (!organization.canEditAllCiphers && cipherFromCipherService != null) { | ||
return cipherFromCipherService; | ||
} | ||
|
||
// Retrieve the cipher through the means of an admin | ||
const cipherResponse = await this.apiService.getCipherAdmin(id); | ||
cipherResponse.edit = true; | ||
|
||
const cipherData = new CipherData(cipherResponse); | ||
return new Cipher(cipherData); | ||
} | ||
} |
Oops, something went wrong.