mirror of
https://github.com/bitwarden/browser
synced 2026-03-01 19:11:22 +00:00
[PM-18855] Add edit Cipher permission check to Cipher Authorization Service and use in Vault dialog (#18375)
Centralize edit permission checks in CipherAuthorizationService instead of using the disableForm parameter passed to VaultItemDialogComponent. This refactoring improves consistency with how delete and restore permissions are handled, establishes a single source of truth for authorization logic, and simplifies caller components. This change also fixes the bug in ticket, which allows Users to properly edit Ciphers inside of the various Admin Console report types.
This commit is contained in:
@@ -920,14 +920,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
cipher?: CipherView,
|
||||
activeCollectionId?: CollectionId,
|
||||
) {
|
||||
const organization = await firstValueFrom(this.organization$);
|
||||
const disableForm = cipher ? !cipher.edit && !organization.canEditAllCiphers : false;
|
||||
// If the form is disabled, force the mode into `view`
|
||||
const dialogMode = disableForm ? "view" : mode;
|
||||
this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, {
|
||||
mode: dialogMode,
|
||||
mode,
|
||||
formConfig,
|
||||
disableForm,
|
||||
activeCollectionId,
|
||||
isAdminConsoleAction: true,
|
||||
restore: this.restore,
|
||||
|
||||
@@ -186,13 +186,10 @@ export abstract class CipherReportComponent implements OnDestroy {
|
||||
cipher: CipherView,
|
||||
activeCollectionId?: CollectionId,
|
||||
) {
|
||||
const disableForm = cipher ? !cipher.edit && !this.organization?.canEditAllCiphers : false;
|
||||
|
||||
this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, {
|
||||
mode,
|
||||
formConfig,
|
||||
activeCollectionId,
|
||||
disableForm,
|
||||
isAdminConsoleAction: this.organization != null,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
@@ -51,7 +47,7 @@ export class ExposedPasswordsReportComponent
|
||||
extends BaseExposedPasswordsReportComponent
|
||||
implements OnInit
|
||||
{
|
||||
manageableCiphers: Cipher[];
|
||||
private manageableCiphers: Cipher[] = [];
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
@@ -82,20 +78,25 @@ export class ExposedPasswordsReportComponent
|
||||
|
||||
async ngOnInit() {
|
||||
this.isAdminConsoleActive = true;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(params.organizationId)),
|
||||
);
|
||||
this.manageableCiphers = await this.cipherService.getAll(userId);
|
||||
});
|
||||
this.route.parent?.parent?.params
|
||||
.pipe(
|
||||
tap(async (params) => {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService.organizations$(userId).pipe(getById(params.organizationId)),
|
||||
);
|
||||
this.manageableCiphers = await this.cipherService.getAll(userId);
|
||||
}),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id, true);
|
||||
async getAllCiphers(): Promise<CipherView[]> {
|
||||
if (this.organization) {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id, true);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
canManageCipher(c: CipherView): boolean {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ChangeDetectorRef, Component, OnInit, ChangeDetectionStrategy } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom, map, takeUntil } from "rxjs";
|
||||
import { firstValueFrom, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
@@ -81,27 +82,24 @@ export class InactiveTwoFactorReportComponent
|
||||
this.isAdminConsoleActive = true;
|
||||
|
||||
this.route.parent?.parent?.params
|
||||
?.pipe(takeUntil(this.destroyed$))
|
||||
// eslint-disable-next-line rxjs/no-async-subscribe
|
||||
.subscribe(async (params) => {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
if (userId) {
|
||||
.pipe(
|
||||
tap(async (params) => {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService.organizations$(userId).pipe(getById(params.organizationId)),
|
||||
);
|
||||
this.manageableCiphers = await this.cipherService.getAll(userId);
|
||||
await super.ngOnInit();
|
||||
}
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async getAllCiphers(): Promise<CipherView[]> {
|
||||
if (this.organization) {
|
||||
return await this.cipherService.getAllFromApiForOrganization(this.organization.id, true);
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id, true);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, takeUntil, tap } from "rxjs";
|
||||
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
@@ -50,7 +46,7 @@ export class ReusedPasswordsReportComponent
|
||||
extends BaseReusedPasswordsReportComponent
|
||||
implements OnInit
|
||||
{
|
||||
manageableCiphers: Cipher[];
|
||||
manageableCiphers: Cipher[] = [];
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
@@ -79,21 +75,27 @@ export class ReusedPasswordsReportComponent
|
||||
|
||||
async ngOnInit() {
|
||||
this.isAdminConsoleActive = true;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(params.organizationId)),
|
||||
);
|
||||
this.manageableCiphers = await this.cipherService.getAll(userId);
|
||||
await super.ngOnInit();
|
||||
});
|
||||
|
||||
this.route.parent?.parent?.params
|
||||
.pipe(
|
||||
tap(async (params) => {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService.organizations$(userId).pipe(getById(params.organizationId)),
|
||||
);
|
||||
this.manageableCiphers = await this.cipherService.getAll(userId);
|
||||
await super.ngOnInit();
|
||||
}),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id, true);
|
||||
async getAllCiphers(): Promise<CipherView[]> {
|
||||
if (this.organization) {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id, true);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
canManageCipher(c: CipherView): boolean {
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
@@ -51,7 +48,7 @@ export class UnsecuredWebsitesReportComponent
|
||||
implements OnInit
|
||||
{
|
||||
// Contains a list of ciphers, the user running the report, can manage
|
||||
private manageableCiphers: Cipher[];
|
||||
private manageableCiphers: Cipher[] = [];
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
@@ -82,23 +79,26 @@ export class UnsecuredWebsitesReportComponent
|
||||
|
||||
async ngOnInit() {
|
||||
this.isAdminConsoleActive = true;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(params.organizationId)),
|
||||
);
|
||||
this.manageableCiphers = await this.cipherService.getAll(userId);
|
||||
await super.ngOnInit();
|
||||
});
|
||||
this.route.parent?.parent?.params
|
||||
.pipe(
|
||||
tap(async (params) => {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService.organizations$(userId).pipe(getById(params.organizationId)),
|
||||
);
|
||||
this.manageableCiphers = await this.cipherService.getAll(userId);
|
||||
await super.ngOnInit();
|
||||
}),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id, true);
|
||||
async getAllCiphers(): Promise<CipherView[]> {
|
||||
if (this.organization) {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id, true);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
protected canManageCipher(c: CipherView): boolean {
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, takeUntil, tap } from "rxjs";
|
||||
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
@@ -51,7 +47,7 @@ export class WeakPasswordsReportComponent
|
||||
extends BaseWeakPasswordsReportComponent
|
||||
implements OnInit
|
||||
{
|
||||
manageableCiphers: Cipher[];
|
||||
private manageableCiphers: Cipher[] = [];
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
@@ -82,22 +78,26 @@ export class WeakPasswordsReportComponent
|
||||
|
||||
async ngOnInit() {
|
||||
this.isAdminConsoleActive = true;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(params.organizationId)),
|
||||
);
|
||||
this.manageableCiphers = await this.cipherService.getAll(userId);
|
||||
await super.ngOnInit();
|
||||
});
|
||||
this.route.parent?.parent?.params
|
||||
.pipe(
|
||||
tap(async (params) => {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService.organizations$(userId).pipe(getById(params.organizationId)),
|
||||
);
|
||||
this.manageableCiphers = await this.cipherService.getAll(userId);
|
||||
await super.ngOnInit();
|
||||
}),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id, true);
|
||||
async getAllCiphers(): Promise<CipherView[]> {
|
||||
if (this.organization) {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id, true);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
canManageCipher(c: CipherView): boolean {
|
||||
|
||||
@@ -87,11 +87,6 @@ export interface VaultItemDialogParams {
|
||||
*/
|
||||
formConfig: CipherFormConfig;
|
||||
|
||||
/**
|
||||
* If true, the "edit" button will be disabled in the dialog.
|
||||
*/
|
||||
disableForm?: boolean;
|
||||
|
||||
/**
|
||||
* The ID of the active collection. This is know the collection filter selected by the user.
|
||||
*/
|
||||
@@ -273,7 +268,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
protected get disableEdit() {
|
||||
return this.params.disableForm;
|
||||
return !this.canEdit;
|
||||
}
|
||||
|
||||
protected get showEdit() {
|
||||
@@ -314,6 +309,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected canDelete = false;
|
||||
|
||||
protected canEdit = false;
|
||||
|
||||
protected attachmentsButtonDisabled = false;
|
||||
|
||||
protected confirmedPremiumUpgrade = false;
|
||||
@@ -372,6 +369,20 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
),
|
||||
);
|
||||
|
||||
this.canEdit = await firstValueFrom(
|
||||
this.cipherAuthorizationService.canEditCipher$(
|
||||
this.cipher,
|
||||
this.params.isAdminConsoleAction,
|
||||
),
|
||||
);
|
||||
|
||||
// If user cannot edit and dialog opened in form mode, force to view mode
|
||||
if (!this.canEdit && this.params.mode === "form") {
|
||||
this.params.mode = "view";
|
||||
this.loadForm = false;
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
await this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientViewed,
|
||||
this.cipher.id,
|
||||
|
||||
Reference in New Issue
Block a user