mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 06:43:35 +00:00
[PM-18089] Update cipher permissions model and consumers (#13606)
* update cipher permissions model and consumers * add new property to tests * fix test, add property to toCipherData() * add missing ConfigService * fix story * refactor * fix error, cleanup * revert refactor * refactor * remove uneeded test * cleanup * fix build error * refactor * clean up * add tests * move validation check to after featrue flagged logic * iterate on feedback * feedback
This commit is contained in:
@@ -1281,6 +1281,7 @@ export default class MainBackground {
|
|||||||
this.collectionService,
|
this.collectionService,
|
||||||
this.organizationService,
|
this.organizationService,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
|
this.configService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
||||||
|
|||||||
@@ -845,6 +845,7 @@ export class ServiceContainer {
|
|||||||
this.collectionService,
|
this.collectionService,
|
||||||
this.organizationService,
|
this.organizationService,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
|
this.configService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.masterPasswordApiService = new MasterPasswordApiService(this.apiService, this.logService);
|
this.masterPasswordApiService = new MasterPasswordApiService(this.apiService, this.logService);
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
|||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
@@ -231,7 +233,10 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
|||||||
* A user may restore items if they have delete permissions and the item is in the trash.
|
* A user may restore items if they have delete permissions and the item is in the trash.
|
||||||
*/
|
*/
|
||||||
protected async canUserRestore() {
|
protected async canUserRestore() {
|
||||||
return this.isTrashFilter && this.cipher?.isDeleted && this.canDelete;
|
const featureFlagEnabled = await firstValueFrom(this.limitItemDeletion$);
|
||||||
|
return this.isTrashFilter && this.cipher?.isDeleted && featureFlagEnabled
|
||||||
|
? this.cipher?.permissions.restore
|
||||||
|
: this.canDelete;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected showRestore: boolean;
|
protected showRestore: boolean;
|
||||||
@@ -277,6 +282,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
protected canDelete = false;
|
protected canDelete = false;
|
||||||
|
|
||||||
|
protected limitItemDeletion$ = this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DIALOG_DATA) protected params: VaultItemDialogParams,
|
@Inject(DIALOG_DATA) protected params: VaultItemDialogParams,
|
||||||
private dialogRef: DialogRef<VaultItemDialogResult>,
|
private dialogRef: DialogRef<VaultItemDialogResult>,
|
||||||
@@ -294,6 +301,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
|||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private eventCollectionService: EventCollectionService,
|
private eventCollectionService: EventCollectionService,
|
||||||
private routedVaultFilterService: RoutedVaultFilterService,
|
private routedVaultFilterService: RoutedVaultFilterService,
|
||||||
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.updateTitle();
|
this.updateTitle();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,12 @@
|
|||||||
appStopProp
|
appStopProp
|
||||||
></button>
|
></button>
|
||||||
<bit-menu #corruptedCipherOptions>
|
<bit-menu #corruptedCipherOptions>
|
||||||
<button bitMenuItem *ngIf="canManageCollection" (click)="deleteCipher()" type="button">
|
<button
|
||||||
|
bitMenuItem
|
||||||
|
*ngIf="(limitItemDeletion$ | async) ? canDeleteCipher : canManageCollection"
|
||||||
|
(click)="deleteCipher()"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
<span class="tw-text-danger">
|
<span class="tw-text-danger">
|
||||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||||
{{ (cipher.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
{{ (cipher.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
||||||
@@ -151,11 +156,21 @@
|
|||||||
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
||||||
{{ "eventLogs" | i18n }}
|
{{ "eventLogs" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button bitMenuItem (click)="restore()" type="button" *ngIf="cipher.isDeleted">
|
<button
|
||||||
|
bitMenuItem
|
||||||
|
(click)="restore()"
|
||||||
|
type="button"
|
||||||
|
*ngIf="(limitItemDeletion$ | async) ? cipher.isDeleted && canRestoreCipher : cipher.isDeleted"
|
||||||
|
>
|
||||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||||
{{ "restore" | i18n }}
|
{{ "restore" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button bitMenuItem *ngIf="canManageCollection" (click)="deleteCipher()" type="button">
|
<button
|
||||||
|
bitMenuItem
|
||||||
|
*ngIf="(limitItemDeletion$ | async) ? canDeleteCipher : canManageCollection"
|
||||||
|
(click)="deleteCipher()"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
<span class="tw-text-danger">
|
<span class="tw-text-danger">
|
||||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||||
{{ (cipher.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
{{ (cipher.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
|||||||
|
|
||||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@@ -36,12 +38,21 @@ export class VaultCipherRowComponent implements OnInit {
|
|||||||
@Input() canEditCipher: boolean;
|
@Input() canEditCipher: boolean;
|
||||||
@Input() canAssignCollections: boolean;
|
@Input() canAssignCollections: boolean;
|
||||||
@Input() canManageCollection: boolean;
|
@Input() canManageCollection: boolean;
|
||||||
|
/**
|
||||||
|
* uses new permission delete logic from PM-15493
|
||||||
|
*/
|
||||||
|
@Input() canDeleteCipher: boolean;
|
||||||
|
/**
|
||||||
|
* uses new permission restore logic from PM-15493
|
||||||
|
*/
|
||||||
|
@Input() canRestoreCipher: boolean;
|
||||||
|
|
||||||
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
||||||
|
|
||||||
@Input() checked: boolean;
|
@Input() checked: boolean;
|
||||||
@Output() checkedToggled = new EventEmitter<void>();
|
@Output() checkedToggled = new EventEmitter<void>();
|
||||||
|
|
||||||
|
protected limitItemDeletion$ = this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion);
|
||||||
protected CipherType = CipherType;
|
protected CipherType = CipherType;
|
||||||
private permissionList = getPermissionList();
|
private permissionList = getPermissionList();
|
||||||
private permissionPriority = [
|
private permissionPriority = [
|
||||||
@@ -53,7 +64,10 @@ export class VaultCipherRowComponent implements OnInit {
|
|||||||
];
|
];
|
||||||
protected organization?: Organization;
|
protected organization?: Organization;
|
||||||
|
|
||||||
constructor(private i18nService: I18nService) {}
|
constructor(
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lifecycle hook for component initialization.
|
* Lifecycle hook for component initialization.
|
||||||
|
|||||||
@@ -86,11 +86,23 @@
|
|||||||
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
||||||
{{ "assignToCollections" | i18n }}
|
{{ "assignToCollections" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="showBulkTrashOptions" type="button" bitMenuItem (click)="bulkRestore()">
|
<button
|
||||||
|
*ngIf="
|
||||||
|
(limitItemDeletion$ | async) ? (canRestoreSelected$ | async) : showBulkTrashOptions
|
||||||
|
"
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="bulkRestore()"
|
||||||
|
>
|
||||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||||
{{ "restoreSelected" | i18n }}
|
{{ "restoreSelected" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="showDelete" type="button" bitMenuItem (click)="bulkDelete()">
|
<button
|
||||||
|
*ngIf="(limitItemDeletion$ | async) ? (canDeleteSelected$ | async) : showDelete"
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="bulkDelete()"
|
||||||
|
>
|
||||||
<span class="tw-text-danger">
|
<span class="tw-text-danger">
|
||||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||||
{{ (showBulkTrashOptions ? "permanentlyDeleteSelected" : "delete") | i18n }}
|
{{ (showBulkTrashOptions ? "permanentlyDeleteSelected" : "delete") | i18n }}
|
||||||
@@ -146,6 +158,16 @@
|
|||||||
[canEditCipher]="canEditCipher(item.cipher)"
|
[canEditCipher]="canEditCipher(item.cipher)"
|
||||||
[canAssignCollections]="canAssignCollections(item.cipher)"
|
[canAssignCollections]="canAssignCollections(item.cipher)"
|
||||||
[canManageCollection]="canManageCollection(item.cipher)"
|
[canManageCollection]="canManageCollection(item.cipher)"
|
||||||
|
[canDeleteCipher]="
|
||||||
|
cipherAuthorizationService.canDeleteCipher$(
|
||||||
|
item.cipher,
|
||||||
|
[item.cipher.collectionId],
|
||||||
|
showAdminActions
|
||||||
|
) | async
|
||||||
|
"
|
||||||
|
[canRestoreCipher]="
|
||||||
|
cipherAuthorizationService.canRestoreCipher$(item.cipher, showAdminActions) | async
|
||||||
|
"
|
||||||
(checkedToggled)="selection.toggle(item)"
|
(checkedToggled)="selection.toggle(item)"
|
||||||
(onEvent)="event($event)"
|
(onEvent)="event($event)"
|
||||||
></tr>
|
></tr>
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { SelectionModel } from "@angular/cdk/collections";
|
import { SelectionModel } from "@angular/cdk/collections";
|
||||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
import { Observable, combineLatest, map, of, startWith, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
|
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
import { SortDirection, TableDataSource } from "@bitwarden/components";
|
import { SortDirection, TableDataSource } from "@bitwarden/components";
|
||||||
|
|
||||||
import { GroupView } from "../../../admin-console/organizations/core";
|
import { GroupView } from "../../../admin-console/organizations/core";
|
||||||
@@ -75,9 +79,67 @@ export class VaultItemsComponent {
|
|||||||
|
|
||||||
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
||||||
|
|
||||||
|
protected limitItemDeletion$ = this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion);
|
||||||
protected editableItems: VaultItem[] = [];
|
protected editableItems: VaultItem[] = [];
|
||||||
protected dataSource = new TableDataSource<VaultItem>();
|
protected dataSource = new TableDataSource<VaultItem>();
|
||||||
protected selection = new SelectionModel<VaultItem>(true, [], true);
|
protected selection = new SelectionModel<VaultItem>(true, [], true);
|
||||||
|
protected canDeleteSelected$: Observable<boolean>;
|
||||||
|
protected canRestoreSelected$: Observable<boolean>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
) {
|
||||||
|
this.canDeleteSelected$ = this.selection.changed.pipe(
|
||||||
|
startWith(null),
|
||||||
|
switchMap(() => {
|
||||||
|
const ciphers = this.selection.selected
|
||||||
|
.filter((item) => item.cipher)
|
||||||
|
.map((item) => item.cipher);
|
||||||
|
|
||||||
|
if (this.selection.selected.length === 0) {
|
||||||
|
return of(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const canDeleteCiphers$ = ciphers.map((c) =>
|
||||||
|
cipherAuthorizationService.canDeleteCipher$(c, [], this.showAdminActions),
|
||||||
|
);
|
||||||
|
|
||||||
|
const canDeleteCollections = this.selection.selected
|
||||||
|
.filter((item) => item.collection)
|
||||||
|
.every((item) => item.collection && this.canDeleteCollection(item.collection));
|
||||||
|
|
||||||
|
const canDelete$ = combineLatest(canDeleteCiphers$).pipe(
|
||||||
|
map((results) => results.every((item) => item) && canDeleteCollections),
|
||||||
|
);
|
||||||
|
|
||||||
|
return canDelete$;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.canRestoreSelected$ = this.selection.changed.pipe(
|
||||||
|
startWith(null),
|
||||||
|
switchMap(() => {
|
||||||
|
const ciphers = this.selection.selected
|
||||||
|
.filter((item) => item.cipher)
|
||||||
|
.map((item) => item.cipher);
|
||||||
|
|
||||||
|
if (this.selection.selected.length === 0) {
|
||||||
|
return of(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const canRestoreCiphers$ = ciphers.map((c) =>
|
||||||
|
cipherAuthorizationService.canRestoreCipher$(c, this.showAdminActions),
|
||||||
|
);
|
||||||
|
|
||||||
|
const canRestore$ = combineLatest(canRestoreCiphers$).pipe(
|
||||||
|
map((results) => results.every((item) => item)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return canRestore$;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get showExtraColumn() {
|
get showExtraColumn() {
|
||||||
return this.showCollections || this.showGroups || this.showOwner;
|
return this.showCollections || this.showGroups || this.showOwner;
|
||||||
@@ -99,6 +161,7 @@ export class VaultItemsComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//@TODO: remove this function when removing the limitItemDeletion$ feature flag.
|
||||||
get showDelete(): boolean {
|
get showDelete(): boolean {
|
||||||
if (this.selection.selected.length === 0) {
|
if (this.selection.selected.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.v
|
|||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||||
|
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
|
|
||||||
import { GroupView } from "../../../admin-console/organizations/core";
|
import { GroupView } from "../../../admin-console/organizations/core";
|
||||||
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
||||||
@@ -104,12 +105,20 @@ export default {
|
|||||||
{
|
{
|
||||||
provide: ConfigService,
|
provide: ConfigService,
|
||||||
useValue: {
|
useValue: {
|
||||||
getFeatureFlag() {
|
getFeatureFlag$() {
|
||||||
// does not currently affect any display logic, default all to OFF
|
// does not currently affect any display logic, default all to OFF
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: CipherAuthorizationService,
|
||||||
|
useValue: {
|
||||||
|
canDeleteCipher$() {
|
||||||
|
return of(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
applicationConfig({
|
applicationConfig({
|
||||||
|
|||||||
@@ -1424,7 +1424,12 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: CipherAuthorizationService,
|
provide: CipherAuthorizationService,
|
||||||
useClass: DefaultCipherAuthorizationService,
|
useClass: DefaultCipherAuthorizationService,
|
||||||
deps: [CollectionService, OrganizationServiceAbstraction, AccountServiceAbstraction],
|
deps: [
|
||||||
|
CollectionService,
|
||||||
|
OrganizationServiceAbstraction,
|
||||||
|
AccountServiceAbstraction,
|
||||||
|
ConfigService,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: AuthRequestApiService,
|
provide: AuthRequestApiService,
|
||||||
|
|||||||
21
libs/common/src/vault/models/api/cipher-permissions.api.ts
Normal file
21
libs/common/src/vault/models/api/cipher-permissions.api.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { BaseResponse } from "../../../models/response/base.response";
|
||||||
|
|
||||||
|
export class CipherPermissionsApi extends BaseResponse {
|
||||||
|
delete: boolean = false;
|
||||||
|
restore: boolean = false;
|
||||||
|
|
||||||
|
constructor(data: any = null) {
|
||||||
|
super(data);
|
||||||
|
if (data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.delete = this.getResponseProperty("Delete");
|
||||||
|
this.restore = this.getResponseProperty("Restore");
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJSON(obj: Jsonify<CipherPermissionsApi>) {
|
||||||
|
return Object.assign(new CipherPermissionsApi(), obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { Jsonify } from "type-fest";
|
|||||||
|
|
||||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||||
import { CipherType } from "../../enums/cipher-type";
|
import { CipherType } from "../../enums/cipher-type";
|
||||||
|
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
|
||||||
import { CipherResponse } from "../response/cipher.response";
|
import { CipherResponse } from "../response/cipher.response";
|
||||||
|
|
||||||
import { AttachmentData } from "./attachment.data";
|
import { AttachmentData } from "./attachment.data";
|
||||||
@@ -21,6 +22,7 @@ export class CipherData {
|
|||||||
folderId: string;
|
folderId: string;
|
||||||
edit: boolean;
|
edit: boolean;
|
||||||
viewPassword: boolean;
|
viewPassword: boolean;
|
||||||
|
permissions: CipherPermissionsApi;
|
||||||
organizationUseTotp: boolean;
|
organizationUseTotp: boolean;
|
||||||
favorite: boolean;
|
favorite: boolean;
|
||||||
revisionDate: string;
|
revisionDate: string;
|
||||||
@@ -51,6 +53,7 @@ export class CipherData {
|
|||||||
this.folderId = response.folderId;
|
this.folderId = response.folderId;
|
||||||
this.edit = response.edit;
|
this.edit = response.edit;
|
||||||
this.viewPassword = response.viewPassword;
|
this.viewPassword = response.viewPassword;
|
||||||
|
this.permissions = response.permissions;
|
||||||
this.organizationUseTotp = response.organizationUseTotp;
|
this.organizationUseTotp = response.organizationUseTotp;
|
||||||
this.favorite = response.favorite;
|
this.favorite = response.favorite;
|
||||||
this.revisionDate = response.revisionDate;
|
this.revisionDate = response.revisionDate;
|
||||||
@@ -95,6 +98,8 @@ export class CipherData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static fromJSON(obj: Jsonify<CipherData>) {
|
static fromJSON(obj: Jsonify<CipherData>) {
|
||||||
return Object.assign(new CipherData(), obj);
|
const result = Object.assign(new CipherData(), obj);
|
||||||
|
result.permissions = CipherPermissionsApi.fromJSON(obj.permissions);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { SecureNote } from "../../models/domain/secure-note";
|
|||||||
import { CardView } from "../../models/view/card.view";
|
import { CardView } from "../../models/view/card.view";
|
||||||
import { IdentityView } from "../../models/view/identity.view";
|
import { IdentityView } from "../../models/view/identity.view";
|
||||||
import { LoginView } from "../../models/view/login.view";
|
import { LoginView } from "../../models/view/login.view";
|
||||||
|
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
|
||||||
|
|
||||||
describe("Cipher DTO", () => {
|
describe("Cipher DTO", () => {
|
||||||
it("Convert from empty CipherData", () => {
|
it("Convert from empty CipherData", () => {
|
||||||
@@ -54,6 +55,7 @@ describe("Cipher DTO", () => {
|
|||||||
fields: null,
|
fields: null,
|
||||||
passwordHistory: null,
|
passwordHistory: null,
|
||||||
key: null,
|
key: null,
|
||||||
|
permissions: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,6 +77,7 @@ describe("Cipher DTO", () => {
|
|||||||
notes: "EncryptedString",
|
notes: "EncryptedString",
|
||||||
creationDate: "2022-01-01T12:00:00.000Z",
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
deletedDate: null,
|
deletedDate: null,
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
reprompt: CipherRepromptType.None,
|
reprompt: CipherRepromptType.None,
|
||||||
key: "EncryptedString",
|
key: "EncryptedString",
|
||||||
login: {
|
login: {
|
||||||
@@ -149,6 +152,7 @@ describe("Cipher DTO", () => {
|
|||||||
localData: null,
|
localData: null,
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
deletedDate: null,
|
deletedDate: null,
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
reprompt: 0,
|
reprompt: 0,
|
||||||
key: { encryptedString: "EncryptedString", encryptionType: 0 },
|
key: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||||
login: {
|
login: {
|
||||||
@@ -228,6 +232,7 @@ describe("Cipher DTO", () => {
|
|||||||
cipher.deletedDate = null;
|
cipher.deletedDate = null;
|
||||||
cipher.reprompt = CipherRepromptType.None;
|
cipher.reprompt = CipherRepromptType.None;
|
||||||
cipher.key = mockEnc("EncKey");
|
cipher.key = mockEnc("EncKey");
|
||||||
|
cipher.permissions = new CipherPermissionsApi();
|
||||||
|
|
||||||
const loginView = new LoginView();
|
const loginView = new LoginView();
|
||||||
loginView.username = "username";
|
loginView.username = "username";
|
||||||
@@ -270,6 +275,7 @@ describe("Cipher DTO", () => {
|
|||||||
deletedDate: null,
|
deletedDate: null,
|
||||||
reprompt: 0,
|
reprompt: 0,
|
||||||
localData: undefined,
|
localData: undefined,
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -297,6 +303,7 @@ describe("Cipher DTO", () => {
|
|||||||
secureNote: {
|
secureNote: {
|
||||||
type: SecureNoteType.Generic,
|
type: SecureNoteType.Generic,
|
||||||
},
|
},
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -326,6 +333,7 @@ describe("Cipher DTO", () => {
|
|||||||
fields: null,
|
fields: null,
|
||||||
passwordHistory: null,
|
passwordHistory: null,
|
||||||
key: { encryptedString: "EncKey", encryptionType: 0 },
|
key: { encryptedString: "EncKey", encryptionType: 0 },
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -353,6 +361,7 @@ describe("Cipher DTO", () => {
|
|||||||
cipher.secureNote = new SecureNote();
|
cipher.secureNote = new SecureNote();
|
||||||
cipher.secureNote.type = SecureNoteType.Generic;
|
cipher.secureNote.type = SecureNoteType.Generic;
|
||||||
cipher.key = mockEnc("EncKey");
|
cipher.key = mockEnc("EncKey");
|
||||||
|
cipher.permissions = new CipherPermissionsApi();
|
||||||
|
|
||||||
const keyService = mock<KeyService>();
|
const keyService = mock<KeyService>();
|
||||||
const encryptService = mock<EncryptService>();
|
const encryptService = mock<EncryptService>();
|
||||||
@@ -387,6 +396,7 @@ describe("Cipher DTO", () => {
|
|||||||
deletedDate: null,
|
deletedDate: null,
|
||||||
reprompt: 0,
|
reprompt: 0,
|
||||||
localData: undefined,
|
localData: undefined,
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -409,6 +419,7 @@ describe("Cipher DTO", () => {
|
|||||||
notes: "EncryptedString",
|
notes: "EncryptedString",
|
||||||
creationDate: "2022-01-01T12:00:00.000Z",
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
deletedDate: null,
|
deletedDate: null,
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
reprompt: CipherRepromptType.None,
|
reprompt: CipherRepromptType.None,
|
||||||
card: {
|
card: {
|
||||||
cardholderName: "EncryptedString",
|
cardholderName: "EncryptedString",
|
||||||
@@ -455,6 +466,7 @@ describe("Cipher DTO", () => {
|
|||||||
fields: null,
|
fields: null,
|
||||||
passwordHistory: null,
|
passwordHistory: null,
|
||||||
key: { encryptedString: "EncKey", encryptionType: 0 },
|
key: { encryptedString: "EncKey", encryptionType: 0 },
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -480,6 +492,7 @@ describe("Cipher DTO", () => {
|
|||||||
cipher.deletedDate = null;
|
cipher.deletedDate = null;
|
||||||
cipher.reprompt = CipherRepromptType.None;
|
cipher.reprompt = CipherRepromptType.None;
|
||||||
cipher.key = mockEnc("EncKey");
|
cipher.key = mockEnc("EncKey");
|
||||||
|
cipher.permissions = new CipherPermissionsApi();
|
||||||
|
|
||||||
const cardView = new CardView();
|
const cardView = new CardView();
|
||||||
cardView.cardholderName = "cardholderName";
|
cardView.cardholderName = "cardholderName";
|
||||||
@@ -522,6 +535,7 @@ describe("Cipher DTO", () => {
|
|||||||
deletedDate: null,
|
deletedDate: null,
|
||||||
reprompt: 0,
|
reprompt: 0,
|
||||||
localData: undefined,
|
localData: undefined,
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -544,6 +558,7 @@ describe("Cipher DTO", () => {
|
|||||||
notes: "EncryptedString",
|
notes: "EncryptedString",
|
||||||
creationDate: "2022-01-01T12:00:00.000Z",
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
deletedDate: null,
|
deletedDate: null,
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
reprompt: CipherRepromptType.None,
|
reprompt: CipherRepromptType.None,
|
||||||
key: "EncKey",
|
key: "EncKey",
|
||||||
identity: {
|
identity: {
|
||||||
@@ -614,6 +629,7 @@ describe("Cipher DTO", () => {
|
|||||||
fields: null,
|
fields: null,
|
||||||
passwordHistory: null,
|
passwordHistory: null,
|
||||||
key: { encryptedString: "EncKey", encryptionType: 0 },
|
key: { encryptedString: "EncKey", encryptionType: 0 },
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -639,6 +655,7 @@ describe("Cipher DTO", () => {
|
|||||||
cipher.deletedDate = null;
|
cipher.deletedDate = null;
|
||||||
cipher.reprompt = CipherRepromptType.None;
|
cipher.reprompt = CipherRepromptType.None;
|
||||||
cipher.key = mockEnc("EncKey");
|
cipher.key = mockEnc("EncKey");
|
||||||
|
cipher.permissions = new CipherPermissionsApi();
|
||||||
|
|
||||||
const identityView = new IdentityView();
|
const identityView = new IdentityView();
|
||||||
identityView.firstName = "firstName";
|
identityView.firstName = "firstName";
|
||||||
@@ -681,6 +698,7 @@ describe("Cipher DTO", () => {
|
|||||||
deletedDate: null,
|
deletedDate: null,
|
||||||
reprompt: 0,
|
reprompt: 0,
|
||||||
localData: undefined,
|
localData: undefined,
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
|
|||||||
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
|
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
|
||||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||||
import { CipherType } from "../../enums/cipher-type";
|
import { CipherType } from "../../enums/cipher-type";
|
||||||
|
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
|
||||||
import { CipherData } from "../data/cipher.data";
|
import { CipherData } from "../data/cipher.data";
|
||||||
import { LocalData } from "../data/local.data";
|
import { LocalData } from "../data/local.data";
|
||||||
import { AttachmentView } from "../view/attachment.view";
|
import { AttachmentView } from "../view/attachment.view";
|
||||||
@@ -39,6 +40,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
|||||||
organizationUseTotp: boolean;
|
organizationUseTotp: boolean;
|
||||||
edit: boolean;
|
edit: boolean;
|
||||||
viewPassword: boolean;
|
viewPassword: boolean;
|
||||||
|
permissions: CipherPermissionsApi;
|
||||||
revisionDate: Date;
|
revisionDate: Date;
|
||||||
localData: LocalData;
|
localData: LocalData;
|
||||||
login: Login;
|
login: Login;
|
||||||
@@ -84,6 +86,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
|||||||
} else {
|
} else {
|
||||||
this.viewPassword = true; // Default for already synced Ciphers without viewPassword
|
this.viewPassword = true; // Default for already synced Ciphers without viewPassword
|
||||||
}
|
}
|
||||||
|
this.permissions = obj.permissions;
|
||||||
this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null;
|
this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null;
|
||||||
this.collectionIds = obj.collectionIds;
|
this.collectionIds = obj.collectionIds;
|
||||||
this.localData = localData;
|
this.localData = localData;
|
||||||
@@ -244,6 +247,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
|||||||
c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : null;
|
c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : null;
|
||||||
c.reprompt = this.reprompt;
|
c.reprompt = this.reprompt;
|
||||||
c.key = this.key?.encryptedString;
|
c.key = this.key?.encryptedString;
|
||||||
|
c.permissions = this.permissions;
|
||||||
|
|
||||||
this.buildDataModel(this, c, {
|
this.buildDataModel(this, c, {
|
||||||
name: null,
|
name: null,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { BaseResponse } from "../../../models/response/base.response";
|
import { BaseResponse } from "../../../models/response/base.response";
|
||||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||||
import { CardApi } from "../api/card.api";
|
import { CardApi } from "../api/card.api";
|
||||||
|
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
|
||||||
import { FieldApi } from "../api/field.api";
|
import { FieldApi } from "../api/field.api";
|
||||||
import { IdentityApi } from "../api/identity.api";
|
import { IdentityApi } from "../api/identity.api";
|
||||||
import { LoginApi } from "../api/login.api";
|
import { LoginApi } from "../api/login.api";
|
||||||
@@ -28,6 +29,7 @@ export class CipherResponse extends BaseResponse {
|
|||||||
favorite: boolean;
|
favorite: boolean;
|
||||||
edit: boolean;
|
edit: boolean;
|
||||||
viewPassword: boolean;
|
viewPassword: boolean;
|
||||||
|
permissions: CipherPermissionsApi;
|
||||||
organizationUseTotp: boolean;
|
organizationUseTotp: boolean;
|
||||||
revisionDate: string;
|
revisionDate: string;
|
||||||
attachments: AttachmentResponse[];
|
attachments: AttachmentResponse[];
|
||||||
@@ -53,6 +55,7 @@ export class CipherResponse extends BaseResponse {
|
|||||||
} else {
|
} else {
|
||||||
this.viewPassword = this.getResponseProperty("ViewPassword");
|
this.viewPassword = this.getResponseProperty("ViewPassword");
|
||||||
}
|
}
|
||||||
|
this.permissions = new CipherPermissionsApi(this.getResponseProperty("Permissions"));
|
||||||
this.organizationUseTotp = this.getResponseProperty("OrganizationUseTotp");
|
this.organizationUseTotp = this.getResponseProperty("OrganizationUseTotp");
|
||||||
this.revisionDate = this.getResponseProperty("RevisionDate");
|
this.revisionDate = this.getResponseProperty("RevisionDate");
|
||||||
this.collectionIds = this.getResponseProperty("CollectionIds");
|
this.collectionIds = this.getResponseProperty("CollectionIds");
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { InitializerKey } from "../../../platform/services/cryptography/initiali
|
|||||||
import { DeepJsonify } from "../../../types/deep-jsonify";
|
import { DeepJsonify } from "../../../types/deep-jsonify";
|
||||||
import { CipherType, LinkedIdType } from "../../enums";
|
import { CipherType, LinkedIdType } from "../../enums";
|
||||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||||
|
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
|
||||||
import { LocalData } from "../data/local.data";
|
import { LocalData } from "../data/local.data";
|
||||||
import { Cipher } from "../domain/cipher";
|
import { Cipher } from "../domain/cipher";
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
type: CipherType = null;
|
type: CipherType = null;
|
||||||
favorite = false;
|
favorite = false;
|
||||||
organizationUseTotp = false;
|
organizationUseTotp = false;
|
||||||
|
permissions: CipherPermissionsApi = new CipherPermissionsApi();
|
||||||
edit = false;
|
edit = false;
|
||||||
viewPassword = true;
|
viewPassword = true;
|
||||||
localData: LocalData;
|
localData: LocalData;
|
||||||
@@ -63,6 +65,7 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
this.organizationUseTotp = c.organizationUseTotp;
|
this.organizationUseTotp = c.organizationUseTotp;
|
||||||
this.edit = c.edit;
|
this.edit = c.edit;
|
||||||
this.viewPassword = c.viewPassword;
|
this.viewPassword = c.viewPassword;
|
||||||
|
this.permissions = c.permissions;
|
||||||
this.type = c.type;
|
this.type = c.type;
|
||||||
this.localData = c.localData;
|
this.localData = c.localData;
|
||||||
this.collectionIds = c.collectionIds;
|
this.collectionIds = c.collectionIds;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|||||||
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
|
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec";
|
import { FakeAccountService, mockAccountServiceWith } from "../../../spec";
|
||||||
|
import { ConfigService } from "../../platform/abstractions/config/config.service";
|
||||||
|
import { CipherPermissionsApi } from "../models/api/cipher-permissions.api";
|
||||||
import { CipherView } from "../models/view/cipher.view";
|
import { CipherView } from "../models/view/cipher.view";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +22,7 @@ describe("CipherAuthorizationService", () => {
|
|||||||
|
|
||||||
const mockCollectionService = mock<CollectionService>();
|
const mockCollectionService = mock<CollectionService>();
|
||||||
const mockOrganizationService = mock<OrganizationService>();
|
const mockOrganizationService = mock<OrganizationService>();
|
||||||
|
const mockConfigService = mock<ConfigService>();
|
||||||
const mockUserId = Utils.newGuid() as UserId;
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
let mockAccountService: FakeAccountService;
|
let mockAccountService: FakeAccountService;
|
||||||
|
|
||||||
@@ -28,10 +31,12 @@ describe("CipherAuthorizationService", () => {
|
|||||||
organizationId: string | null,
|
organizationId: string | null,
|
||||||
collectionIds: string[],
|
collectionIds: string[],
|
||||||
edit: boolean = true,
|
edit: boolean = true,
|
||||||
|
permissions: CipherPermissionsApi = new CipherPermissionsApi(),
|
||||||
) => ({
|
) => ({
|
||||||
organizationId,
|
organizationId,
|
||||||
collectionIds,
|
collectionIds,
|
||||||
edit,
|
edit,
|
||||||
|
permissions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createMockCollection = (id: string, manage: boolean) => ({
|
const createMockCollection = (id: string, manage: boolean) => ({
|
||||||
@@ -63,7 +68,78 @@ describe("CipherAuthorizationService", () => {
|
|||||||
mockCollectionService,
|
mockCollectionService,
|
||||||
mockOrganizationService,
|
mockOrganizationService,
|
||||||
mockAccountService,
|
mockAccountService,
|
||||||
|
mockConfigService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
mockConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("canRestoreCipher$", () => {
|
||||||
|
it("should return true if isAdminConsoleAction and cipher is unassigned", (done) => {
|
||||||
|
const cipher = createMockCipher("org1", []) as CipherView;
|
||||||
|
const organization = createMockOrganization({ canEditUnassignedCiphers: true });
|
||||||
|
mockOrganizationService.organizations$.mockReturnValue(
|
||||||
|
of([organization]) as Observable<Organization[]>,
|
||||||
|
);
|
||||||
|
|
||||||
|
cipherAuthorizationService.canRestoreCipher$(cipher, true).subscribe((result) => {
|
||||||
|
expect(result).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true if isAdminConsleAction and user can edit all ciphers in the org", (done) => {
|
||||||
|
const cipher = createMockCipher("org1", ["col1"]) as CipherView;
|
||||||
|
const organization = createMockOrganization({ canEditAllCiphers: true });
|
||||||
|
mockOrganizationService.organizations$.mockReturnValue(
|
||||||
|
of([organization]) as Observable<Organization[]>,
|
||||||
|
);
|
||||||
|
|
||||||
|
cipherAuthorizationService.canRestoreCipher$(cipher, true).subscribe((result) => {
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockOrganizationService.organizations$).toHaveBeenCalledWith(mockUserId);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if isAdminConsoleAction is true but user does not have permission to edit unassigned ciphers", (done) => {
|
||||||
|
const cipher = createMockCipher("org1", []) as CipherView;
|
||||||
|
const organization = createMockOrganization({ canEditUnassignedCiphers: false });
|
||||||
|
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
|
||||||
|
|
||||||
|
cipherAuthorizationService.canRestoreCipher$(cipher, true).subscribe((result) => {
|
||||||
|
expect(result).toBe(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if cipher.permission.restore is false and is not an admin action", (done) => {
|
||||||
|
const cipher = createMockCipher("org1", [], true, {
|
||||||
|
restore: false,
|
||||||
|
} as CipherPermissionsApi) as CipherView;
|
||||||
|
const organization = createMockOrganization();
|
||||||
|
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
|
||||||
|
|
||||||
|
cipherAuthorizationService.canRestoreCipher$(cipher, false).subscribe((result) => {
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(mockCollectionService.decryptedCollectionViews$).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true if cipher.permission.restore is true and is not an admin action", (done) => {
|
||||||
|
const cipher = createMockCipher("org1", [], true, {
|
||||||
|
restore: true,
|
||||||
|
} as CipherPermissionsApi) as CipherView;
|
||||||
|
const organization = createMockOrganization();
|
||||||
|
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
|
||||||
|
|
||||||
|
cipherAuthorizationService.canRestoreCipher$(cipher, false).subscribe((result) => {
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockCollectionService.decryptedCollectionViews$).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("canDeleteCipher$", () => {
|
describe("canDeleteCipher$", () => {
|
||||||
@@ -213,6 +289,34 @@ describe("CipherAuthorizationService", () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return true if feature flag enabled and cipher.permissions.delete is true", (done) => {
|
||||||
|
const cipher = createMockCipher("org1", [], true, {
|
||||||
|
delete: true,
|
||||||
|
} as CipherPermissionsApi) as CipherView;
|
||||||
|
const organization = createMockOrganization();
|
||||||
|
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
|
||||||
|
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||||
|
|
||||||
|
cipherAuthorizationService.canDeleteCipher$(cipher, [], false).subscribe((result) => {
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockCollectionService.decryptedCollectionViews$).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if feature flag enabled and cipher.permissions.delete is false", (done) => {
|
||||||
|
const cipher = createMockCipher("org1", []) as CipherView;
|
||||||
|
const organization = createMockOrganization();
|
||||||
|
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
|
||||||
|
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||||
|
|
||||||
|
cipherAuthorizationService.canDeleteCipher$(cipher, [], false).subscribe((result) => {
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(mockCollectionService.decryptedCollectionViews$).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("canCloneCipher$", () => {
|
describe("canCloneCipher$", () => {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
import { combineLatest, map, Observable, of, shareReplay, switchMap } from "rxjs";
|
||||||
// @ts-strict-ignore
|
|
||||||
import { map, Observable, of, shareReplay, switchMap } from "rxjs";
|
|
||||||
|
|
||||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||||
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { getUserId } from "../../auth/services/account.service";
|
||||||
|
import { FeatureFlag } from "../../enums/feature-flag.enum";
|
||||||
import { Cipher } from "../models/domain/cipher";
|
import { Cipher } from "../models/domain/cipher";
|
||||||
import { CipherView } from "../models/view/cipher.view";
|
import { CipherView } from "../models/view/cipher.view";
|
||||||
|
|
||||||
@@ -28,12 +29,25 @@ export abstract class CipherAuthorizationService {
|
|||||||
*
|
*
|
||||||
* @returns {Observable<boolean>} - An observable that emits a boolean value indicating if the user can delete the cipher.
|
* @returns {Observable<boolean>} - An observable that emits a boolean value indicating if the user can delete the cipher.
|
||||||
*/
|
*/
|
||||||
canDeleteCipher$: (
|
abstract canDeleteCipher$: (
|
||||||
cipher: CipherLike,
|
cipher: CipherLike,
|
||||||
allowedCollections?: CollectionId[],
|
allowedCollections?: CollectionId[],
|
||||||
isAdminConsoleAction?: boolean,
|
isAdminConsoleAction?: boolean,
|
||||||
) => Observable<boolean>;
|
) => Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the user can restore the specified cipher.
|
||||||
|
*
|
||||||
|
* @param {CipherLike} cipher - The cipher object to evaluate for restore permissions.
|
||||||
|
* @param {boolean} isAdminConsoleAction - Optional. A flag indicating if the action is being performed from the admin console.
|
||||||
|
*
|
||||||
|
* @returns {Observable<boolean>} - An observable that emits a boolean value indicating if the user can restore the cipher.
|
||||||
|
*/
|
||||||
|
abstract canRestoreCipher$: (
|
||||||
|
cipher: CipherLike,
|
||||||
|
isAdminConsoleAction?: boolean,
|
||||||
|
) => Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if the user can clone the specified cipher.
|
* Determines if the user can clone the specified cipher.
|
||||||
*
|
*
|
||||||
@@ -42,7 +56,10 @@ export abstract class CipherAuthorizationService {
|
|||||||
*
|
*
|
||||||
* @returns {Observable<boolean>} - An observable that emits a boolean value indicating if the user can clone the cipher.
|
* @returns {Observable<boolean>} - An observable that emits a boolean value indicating if the user can clone the cipher.
|
||||||
*/
|
*/
|
||||||
canCloneCipher$: (cipher: CipherLike, isAdminConsoleAction?: boolean) => Observable<boolean>;
|
abstract canCloneCipher$: (
|
||||||
|
cipher: CipherLike,
|
||||||
|
isAdminConsoleAction?: boolean,
|
||||||
|
) => Observable<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,13 +70,16 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
|
|||||||
private collectionService: CollectionService,
|
private collectionService: CollectionService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private organization$ = (cipher: CipherLike) =>
|
private organization$ = (cipher: CipherLike) =>
|
||||||
this.accountService.activeAccount$.pipe(
|
this.accountService.activeAccount$.pipe(
|
||||||
switchMap((account) => this.organizationService.organizations$(account?.id)),
|
getUserId,
|
||||||
|
switchMap((userId) => this.organizationService.organizations$(userId)),
|
||||||
map((orgs) => orgs.find((org) => org.id === cipher.organizationId)),
|
map((orgs) => orgs.find((org) => org.id === cipher.organizationId)),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* {@link CipherAuthorizationService.canDeleteCipher$}
|
* {@link CipherAuthorizationService.canDeleteCipher$}
|
||||||
@@ -69,12 +89,11 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
|
|||||||
allowedCollections?: CollectionId[],
|
allowedCollections?: CollectionId[],
|
||||||
isAdminConsoleAction?: boolean,
|
isAdminConsoleAction?: boolean,
|
||||||
): Observable<boolean> {
|
): Observable<boolean> {
|
||||||
if (cipher.organizationId == null) {
|
return combineLatest([
|
||||||
return of(true);
|
this.organization$(cipher),
|
||||||
}
|
this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion),
|
||||||
|
]).pipe(
|
||||||
return this.organization$(cipher).pipe(
|
switchMap(([organization, featureFlagEnabled]) => {
|
||||||
switchMap((organization) => {
|
|
||||||
if (isAdminConsoleAction) {
|
if (isAdminConsoleAction) {
|
||||||
// If the user is an admin, they can delete an unassigned cipher
|
// If the user is an admin, they can delete an unassigned cipher
|
||||||
if (!cipher.collectionIds || cipher.collectionIds.length === 0) {
|
if (!cipher.collectionIds || cipher.collectionIds.length === 0) {
|
||||||
@@ -86,6 +105,14 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (featureFlagEnabled) {
|
||||||
|
return of(cipher.permissions.delete);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cipher.organizationId == null) {
|
||||||
|
return of(true);
|
||||||
|
}
|
||||||
|
|
||||||
return this.collectionService
|
return this.collectionService
|
||||||
.decryptedCollectionViews$(cipher.collectionIds as CollectionId[])
|
.decryptedCollectionViews$(cipher.collectionIds as CollectionId[])
|
||||||
.pipe(
|
.pipe(
|
||||||
@@ -93,7 +120,7 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
|
|||||||
const shouldFilter = allowedCollections?.some(Boolean);
|
const shouldFilter = allowedCollections?.some(Boolean);
|
||||||
|
|
||||||
const collections = shouldFilter
|
const collections = shouldFilter
|
||||||
? allCollections.filter((c) => allowedCollections.includes(c.id as CollectionId))
|
? allCollections.filter((c) => allowedCollections?.includes(c.id as CollectionId))
|
||||||
: allCollections;
|
: allCollections;
|
||||||
|
|
||||||
return collections.some((collection) => collection.manage);
|
return collections.some((collection) => collection.manage);
|
||||||
@@ -103,6 +130,29 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@link CipherAuthorizationService.canRestoreCipher$}
|
||||||
|
*/
|
||||||
|
canRestoreCipher$(cipher: CipherLike, isAdminConsoleAction?: boolean): Observable<boolean> {
|
||||||
|
return this.organization$(cipher).pipe(
|
||||||
|
map((organization) => {
|
||||||
|
if (isAdminConsoleAction) {
|
||||||
|
// If the user is an admin, they can restore an unassigned cipher
|
||||||
|
if (!cipher.collectionIds || cipher.collectionIds.length === 0) {
|
||||||
|
return organization?.canEditUnassignedCiphers === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization?.canEditAllCiphers) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cipher.permissions.restore;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link CipherAuthorizationService.canCloneCipher$}
|
* {@link CipherAuthorizationService.canCloneCipher$}
|
||||||
*/
|
*/
|
||||||
@@ -116,6 +166,7 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
|
|||||||
// Admins and custom users can always clone when in the Admin Console
|
// Admins and custom users can always clone when in the Admin Console
|
||||||
if (
|
if (
|
||||||
isAdminConsoleAction &&
|
isAdminConsoleAction &&
|
||||||
|
organization &&
|
||||||
(organization.isAdmin || organization.permissions?.editAnyCollection)
|
(organization.isAdmin || organization.permissions?.editAnyCollection)
|
||||||
) {
|
) {
|
||||||
return of(true);
|
return of(true);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file
|
|||||||
import { FieldType } from "../enums";
|
import { FieldType } from "../enums";
|
||||||
import { CipherRepromptType } from "../enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "../enums/cipher-reprompt-type";
|
||||||
import { CipherType } from "../enums/cipher-type";
|
import { CipherType } from "../enums/cipher-type";
|
||||||
|
import { CipherPermissionsApi } from "../models/api/cipher-permissions.api";
|
||||||
import { CipherData } from "../models/data/cipher.data";
|
import { CipherData } from "../models/data/cipher.data";
|
||||||
import { Cipher } from "../models/domain/cipher";
|
import { Cipher } from "../models/domain/cipher";
|
||||||
import { CipherCreateRequest } from "../models/request/cipher-create.request";
|
import { CipherCreateRequest } from "../models/request/cipher-create.request";
|
||||||
@@ -57,6 +58,7 @@ const cipherData: CipherData = {
|
|||||||
notes: "EncryptedString",
|
notes: "EncryptedString",
|
||||||
creationDate: "2022-01-01T12:00:00.000Z",
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
deletedDate: null,
|
deletedDate: null,
|
||||||
|
permissions: new CipherPermissionsApi(),
|
||||||
key: "EncKey",
|
key: "EncKey",
|
||||||
reprompt: CipherRepromptType.None,
|
reprompt: CipherRepromptType.None,
|
||||||
login: {
|
login: {
|
||||||
|
|||||||
Reference in New Issue
Block a user