diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index edf93e3c953..687aef9b671 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -853,6 +853,7 @@ export class VaultComponent implements OnInit, OnDestroy { const dialogRef = AttachmentsV2Component.open(this.dialogService, { cipherId: cipher.id as CipherId, organizationId: cipher.organizationId as OrganizationId, + admin: true, }); const result = await firstValueFrom(dialogRef.closed); diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html index 806e35d80d4..83e5956a067 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html @@ -10,14 +10,14 @@ { decrypt: () => cipherView, }; + const organization = new Organization(); + organization.type = OrganizationUserType.Admin; + organization.allowAdminAccessToAllCollectionItems = true; + const cipherServiceGet = jest.fn().mockResolvedValue(cipherDomain); const saveAttachmentWithServer = jest.fn().mockResolvedValue(cipherDomain); @@ -70,6 +76,7 @@ describe("CipherAttachmentsComponent", () => { { provide: CipherService, useValue: { + organization, get: cipherServiceGet, saveAttachmentWithServer, getKeyForCipherKeyDecryption: () => Promise.resolve(null), @@ -240,9 +247,11 @@ describe("CipherAttachmentsComponent", () => { beforeEach(() => { component.attachmentForm.controls.file.setValue(file); + component.organization = organization; }); - it("calls `saveAttachmentWithServer`", async () => { + it("calls `saveAttachmentWithServer` with admin=false when admin permission is false for organization", async () => { + component.organization.allowAdminAccessToAllCollectionItems = false; await component.submit(); expect(saveAttachmentWithServer).toHaveBeenCalledWith( @@ -253,10 +262,8 @@ describe("CipherAttachmentsComponent", () => { ); }); - it("calls `saveAttachmentWithServer` with isAdmin=true when using admin API", async () => { - // Set isAdmin to true to use admin API - Object.defineProperty(component, "isAdmin", { value: true }); - + it("calls `saveAttachmentWithServer` with admin=true when using admin API", async () => { + component.organization.allowAdminAccessToAllCollectionItems = true; await component.submit(); expect(saveAttachmentWithServer).toHaveBeenCalledWith(cipherDomain, file, mockUserId, true); diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts index e381f6f07ea..29a80c826c6 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts @@ -89,6 +89,9 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { /** The organization ID if this cipher belongs to an organization */ @Input() organizationId?: OrganizationId; + /** Denotes if the action is occurring from within the admin console */ + @Input() admin: boolean = false; + /** An optional submit button, whose loading/disabled state will be tied to the form state. */ @Input() submitBtn?: ButtonComponent; @@ -98,6 +101,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { /** Emits after a file has been successfully removed */ @Output() onRemoveSuccess = new EventEmitter(); + organization: Organization; cipher: CipherView; attachmentForm: CipherAttachmentForm = this.formBuilder.group({ @@ -106,7 +110,6 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { private cipherDomain: Cipher; private activeUserId: UserId; - private isAdmin = false; private destroy$ = inject(DestroyRef); constructor( @@ -130,6 +133,8 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { async ngOnInit(): Promise { this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + // Get the organization to check admin permissions + this.organization = await this.getOrganization(); this.cipherDomain = await this.getCipher(this.cipherId); this.cipher = await this.cipherDomain.decrypt( @@ -201,7 +206,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { this.cipherDomain, file, this.activeUserId, - this.isAdmin, + this.organization?.canEditAllCiphers, ); // re-decrypt the cipher to update the attachments @@ -254,11 +259,8 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { return localCipher; } - // Get the organization to check admin permissions - const organization = await this.getOrganization(); // Only try the admin API if the user has admin permissions - if (organization != null && organization.canEditAllCiphers) { - this.isAdmin = true; + if (this.organization != null && this.organization.canEditAllCiphers) { const cipherResponse = await this.apiService.getCipherAdmin(id); const cipherData = new CipherData(cipherResponse); return new Cipher(cipherData); diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2.component.html b/libs/vault/src/cipher-view/attachments/attachments-v2.component.html index 916430ec57b..3b096634069 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2.component.html +++ b/libs/vault/src/cipher-view/attachments/attachments-v2.component.html @@ -7,6 +7,7 @@ *ngIf="cipherId" [cipherId]="cipherId" [organizationId]="organizationId" + [admin]="admin" [submitBtn]="submitBtn" (onUploadSuccess)="uploadSuccessful()" (onRemoveSuccess)="removalSuccessful()" diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts index 25cd4398664..68660c4bbd9 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts +++ b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts @@ -17,6 +17,7 @@ import { CipherAttachmentsComponent } from "../../cipher-form/components/attachm export interface AttachmentsDialogParams { cipherId: CipherId; + admin?: boolean; organizationId?: OrganizationId; } @@ -44,6 +45,7 @@ export interface AttachmentDialogCloseResult { }) export class AttachmentsV2Component { cipherId: CipherId; + admin: boolean = false; organizationId?: OrganizationId; attachmentFormId = CipherAttachmentsComponent.attachmentFormID; @@ -58,6 +60,7 @@ export class AttachmentsV2Component { ) { this.cipherId = params.cipherId; this.organizationId = params.organizationId; + this.admin = params.admin ?? false; } /** diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.ts b/libs/vault/src/components/download-attachment/download-attachment.component.ts index d96a40b0f86..e64777ebb8e 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.ts @@ -39,7 +39,7 @@ export class DownloadAttachmentComponent { // Required for fetching attachment data when viewed from cipher via emergency access @Input() emergencyAccessId?: EmergencyAccessId; - /** When accessing from the admin console, we will want to call the admin endpoint */ + /** When owners/admins can mange all items and when accessing from the admin console, use the admin endpoint */ @Input() admin?: boolean = false; /** The organization key if the cipher is associated with one */