Browse Source
* Add access-policy-selector * Update to access-policy service and models * Add access-policy service tests * Use new selector in project-people * Fix access removal dialog bug (#6653)pull/6834/head
19 changed files with 1089 additions and 169 deletions
@ -1,17 +1,21 @@
@@ -1,17 +1,21 @@
|
||||
<div class="tw-w-2/5"> |
||||
<p class="tw-mt-8"> |
||||
{{ "projectPeopleDescription" | i18n }} |
||||
</p> |
||||
<sm-access-selector |
||||
[rows]="rows$ | async" |
||||
granteeType="people" |
||||
[label]="'people' | i18n" |
||||
[hint]="'projectPeopleSelectHint' | i18n" |
||||
[columnTitle]="'name' | i18n" |
||||
[emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n" |
||||
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)" |
||||
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" |
||||
(onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)" |
||||
> |
||||
</sm-access-selector> |
||||
</div> |
||||
<form [formGroup]="formGroup" [bitSubmit]="submit"> |
||||
<div class="tw-w-2/5"> |
||||
<p class="tw-mt-8" *ngIf="!loading"> |
||||
{{ "projectPeopleDescription" | i18n }} |
||||
</p> |
||||
<sm-access-policy-selector |
||||
[loading]="loading" |
||||
formControlName="accessPolicies" |
||||
[addButtonMode]="true" |
||||
[items]="potentialGrantees" |
||||
[label]="'people' | i18n" |
||||
[hint]="'projectPeopleSelectHint' | i18n" |
||||
[columnTitle]="'name' | i18n" |
||||
[emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n" |
||||
> |
||||
</sm-access-policy-selector> |
||||
<button bitButton buttonType="primary" bitFormButton type="submit" class="tw-mt-7"> |
||||
{{ "save" | i18n }} |
||||
</button> |
||||
</div> |
||||
</form> |
||||
|
||||
@ -0,0 +1,97 @@
@@ -0,0 +1,97 @@
|
||||
<div class="tw-flex"> |
||||
<ng-container *ngIf="!addButtonMode; else buttonMode"> |
||||
<bit-form-field class="tw-grow"> |
||||
<bit-label>{{ label }}</bit-label> |
||||
<bit-multi-select |
||||
class="tw-w-full" |
||||
[loading]="loading" |
||||
[baseItems]="selectionList.deselectedItems" |
||||
[disabled]="disabled" |
||||
[removeSelectedItems]="true" |
||||
(blur)="handleBlur()" |
||||
(onItemsConfirmed)="selectItems($event)" |
||||
></bit-multi-select> |
||||
<bit-hint>{{ hint }}</bit-hint> |
||||
</bit-form-field> |
||||
</ng-container> |
||||
</div> |
||||
|
||||
<bit-table [formGroup]="formGroup" *ngIf="!loading; else spinner"> |
||||
<ng-container header> |
||||
<tr> |
||||
<th bitCell colspan="2">{{ columnTitle }}</th> |
||||
<th bitCell>{{ "permissions" | i18n }}</th> |
||||
</tr> |
||||
</ng-container> |
||||
<ng-template body formArrayName="items"> |
||||
<ng-container *ngIf="selectionList.selectedItems.length > 0; else empty"> |
||||
<tr |
||||
bitRow |
||||
*ngFor="let item of selectionList.selectedItems; let i = index" |
||||
[formGroupName]="i" |
||||
> |
||||
<td bitCell class="tw-w-0 tw-pr-0"> |
||||
<i class="bwi {{ item.icon }} tw-text-muted" aria-hidden="true"></i> |
||||
</td> |
||||
<td bitCell class="tw-max-w-sm tw-truncate">{{ item.labelName }}</td> |
||||
<td bitCell class="tw-mb-auto tw-inline-block tw-w-auto"> |
||||
<select |
||||
*ngIf="!staticPermission; else static" |
||||
bitInput |
||||
formControlName="permission" |
||||
(blur)="handleBlur()" |
||||
> |
||||
<option *ngFor="let p of permissionList" [value]="p.perm"> |
||||
{{ p.labelId | i18n }} |
||||
</option> |
||||
</select> |
||||
<ng-template #static> |
||||
<span>{{ staticPermission | i18n }}</span> |
||||
</ng-template> |
||||
</td> |
||||
<td bitCell class="tw-w-0"> |
||||
<button |
||||
type="button" |
||||
bitIconButton="bwi-close" |
||||
buttonType="main" |
||||
size="default" |
||||
[attr.title]="'remove' | i18n" |
||||
[attr.aria-label]="'remove' | i18n" |
||||
(click)="selectionList.deselectItem(item.id); handleBlur()" |
||||
></button> |
||||
</td> |
||||
</tr> |
||||
</ng-container> |
||||
</ng-template> |
||||
</bit-table> |
||||
|
||||
<ng-template #empty> |
||||
<div class="tw-mt-4 tw-text-center"> |
||||
{{ emptyMessage }} |
||||
</div> |
||||
</ng-template> |
||||
|
||||
<ng-template #buttonMode> |
||||
<bit-form-field class="tw-grow" [formGroup]="multiSelectFormGroup"> |
||||
<bit-label>{{ label }}</bit-label> |
||||
<bit-multi-select |
||||
class="tw-w-full" |
||||
formControlName="multiSelect" |
||||
[baseItems]="selectionList.deselectedItems" |
||||
(blur)="handleBlur()" |
||||
></bit-multi-select> |
||||
<bit-hint>{{ hint }}</bit-hint> |
||||
</bit-form-field> |
||||
|
||||
<div class="tw-ml-3 tw-mt-7 tw-shrink-0"> |
||||
<button type="button" bitButton buttonType="secondary" (click)="addButton()"> |
||||
{{ "add" | i18n }} |
||||
</button> |
||||
</div> |
||||
</ng-template> |
||||
|
||||
<ng-template #spinner> |
||||
<div class="tw-items-center tw-justify-center tw-pt-10 tw-text-center"> |
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i> |
||||
</div> |
||||
</ng-template> |
||||
@ -0,0 +1,222 @@
@@ -0,0 +1,222 @@
|
||||
import { Component, forwardRef, Input, OnDestroy, OnInit } from "@angular/core"; |
||||
import { |
||||
ControlValueAccessor, |
||||
FormBuilder, |
||||
FormControl, |
||||
FormGroup, |
||||
NG_VALUE_ACCESSOR, |
||||
} from "@angular/forms"; |
||||
import { Subject, takeUntil } from "rxjs"; |
||||
|
||||
import { ControlsOf } from "@bitwarden/angular/types/controls-of"; |
||||
import { FormSelectionList } from "@bitwarden/angular/utils/form-selection-list"; |
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; |
||||
import { SelectItemView } from "@bitwarden/components"; |
||||
|
||||
import { ApItemValueType } from "./models/ap-item-value.type"; |
||||
import { ApItemViewType } from "./models/ap-item-view.type"; |
||||
import { ApItemEnumUtil, ApItemEnum } from "./models/enums/ap-item.enum"; |
||||
import { ApPermissionEnum } from "./models/enums/ap-permission.enum"; |
||||
|
||||
@Component({ |
||||
selector: "sm-access-policy-selector", |
||||
templateUrl: "access-policy-selector.component.html", |
||||
providers: [ |
||||
{ |
||||
provide: NG_VALUE_ACCESSOR, |
||||
useExisting: forwardRef(() => AccessPolicySelectorComponent), |
||||
multi: true, |
||||
}, |
||||
], |
||||
}) |
||||
export class AccessPolicySelectorComponent implements ControlValueAccessor, OnInit, OnDestroy { |
||||
private destroy$ = new Subject<void>(); |
||||
private notifyOnChange: (v: unknown) => void; |
||||
private notifyOnTouch: () => void; |
||||
private pauseChangeNotification: boolean; |
||||
|
||||
/** |
||||
* The internal selection list that tracks the value of this form control / component. |
||||
* It's responsible for keeping items sorted and synced with the rendered form controls |
||||
* @protected |
||||
*/ |
||||
protected selectionList = new FormSelectionList<ApItemViewType, ApItemValueType>((item) => { |
||||
const initPermission = this.staticPermission ?? this.initialPermission; |
||||
|
||||
const permissionControl = this.formBuilder.control(initPermission); |
||||
let currentUserInGroup = false; |
||||
let currentUser = false; |
||||
if (item.type == ApItemEnum.Group) { |
||||
currentUserInGroup = item.currentUserInGroup; |
||||
} |
||||
if (item.type == ApItemEnum.User) { |
||||
currentUser = item.currentUser; |
||||
} |
||||
const fg = this.formBuilder.group<ControlsOf<ApItemValueType>>({ |
||||
id: new FormControl(item.id), |
||||
type: new FormControl(item.type), |
||||
permission: permissionControl, |
||||
currentUserInGroup: new FormControl(currentUserInGroup), |
||||
currentUser: new FormControl(currentUser), |
||||
}); |
||||
return fg; |
||||
}, this._itemComparator.bind(this)); |
||||
|
||||
/** |
||||
* Internal form group for this component. |
||||
* @protected |
||||
*/ |
||||
protected formGroup = this.formBuilder.group({ |
||||
items: this.selectionList.formArray, |
||||
}); |
||||
|
||||
protected multiSelectFormGroup = new FormGroup({ |
||||
multiSelect: new FormControl([]), |
||||
}); |
||||
|
||||
disabled: boolean; |
||||
|
||||
@Input() loading: boolean; |
||||
@Input() addButtonMode: boolean; |
||||
@Input() label: string; |
||||
@Input() hint: string; |
||||
@Input() columnTitle: string; |
||||
@Input() emptyMessage: string; |
||||
|
||||
@Input() permissionList = [ |
||||
{ perm: ApPermissionEnum.CanRead, labelId: "canRead" }, |
||||
{ perm: ApPermissionEnum.CanReadWrite, labelId: "canReadWrite" }, |
||||
]; |
||||
@Input() initialPermission = ApPermissionEnum.CanRead; |
||||
|
||||
// Pass in a static permission that wil be the only option for a given selector instance.
|
||||
// Will ignore permissionList and initialPermission.
|
||||
@Input() staticPermission: ApPermissionEnum; |
||||
|
||||
@Input() |
||||
get items(): ApItemViewType[] { |
||||
return this.selectionList.allItems; |
||||
} |
||||
|
||||
set items(val: ApItemViewType[]) { |
||||
if (val != null) { |
||||
const selected = this.selectionList.formArray.getRawValue() ?? []; |
||||
this.selectionList.populateItems( |
||||
val.map((m) => { |
||||
m.icon = m.icon ?? ApItemEnumUtil.itemIcon(m.type); |
||||
return m; |
||||
}), |
||||
selected |
||||
); |
||||
} |
||||
} |
||||
|
||||
constructor( |
||||
private readonly formBuilder: FormBuilder, |
||||
private readonly i18nService: I18nService |
||||
) {} |
||||
|
||||
/** Required for NG_VALUE_ACCESSOR */ |
||||
registerOnChange(fn: any): void { |
||||
this.notifyOnChange = fn; |
||||
} |
||||
|
||||
/** Required for NG_VALUE_ACCESSOR */ |
||||
registerOnTouched(fn: any): void { |
||||
this.notifyOnTouch = fn; |
||||
} |
||||
|
||||
/** Required for NG_VALUE_ACCESSOR */ |
||||
setDisabledState(isDisabled: boolean): void { |
||||
this.disabled = isDisabled; |
||||
|
||||
// Keep the internal FormGroup in sync
|
||||
if (this.disabled) { |
||||
this.formGroup.disable(); |
||||
this.multiSelectFormGroup.disable(); |
||||
} else { |
||||
this.formGroup.enable(); |
||||
this.multiSelectFormGroup.enable(); |
||||
} |
||||
} |
||||
|
||||
/** Required for NG_VALUE_ACCESSOR */ |
||||
writeValue(selectedItems: ApItemValueType[]): void { |
||||
// Modifying the selection list, mistakenly fires valueChanges in the
|
||||
// internal form array, so we need to know to pause external notification
|
||||
this.pauseChangeNotification = true; |
||||
|
||||
// Always clear the internal selection list on a new value
|
||||
this.selectionList.deselectAll(); |
||||
|
||||
// If the new value is null, then we're done
|
||||
if (selectedItems == null) { |
||||
this.pauseChangeNotification = false; |
||||
return; |
||||
} |
||||
|
||||
// Unable to handle other value types, throw
|
||||
if (!Array.isArray(selectedItems)) { |
||||
throw new Error("The access selector component only supports Array form values!"); |
||||
} |
||||
|
||||
// Iterate and internally select each item
|
||||
for (const value of selectedItems) { |
||||
this.selectionList.selectItem(value.id, value); |
||||
} |
||||
|
||||
this.pauseChangeNotification = false; |
||||
} |
||||
|
||||
ngOnInit() { |
||||
// Watch the internal formArray for changes and propagate them
|
||||
this.selectionList.formArray.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((v) => { |
||||
if (!this.notifyOnChange || this.pauseChangeNotification) { |
||||
return; |
||||
} |
||||
|
||||
// Disabled form arrays emit values for disabled controls, we override this to emit an empty array to avoid
|
||||
// emitting values for disabled controls that are "readonly" in the table
|
||||
if (this.selectionList.formArray.disabled) { |
||||
this.notifyOnChange([]); |
||||
return; |
||||
} |
||||
this.notifyOnChange(v); |
||||
}); |
||||
} |
||||
|
||||
ngOnDestroy() { |
||||
this.destroy$.next(); |
||||
this.destroy$.complete(); |
||||
} |
||||
|
||||
protected handleBlur() { |
||||
if (!this.notifyOnTouch) { |
||||
return; |
||||
} |
||||
|
||||
this.notifyOnTouch(); |
||||
} |
||||
|
||||
protected selectItems(items: SelectItemView[]) { |
||||
this.pauseChangeNotification = true; |
||||
this.selectionList.selectItems(items.map((i) => i.id)); |
||||
this.pauseChangeNotification = false; |
||||
if (this.notifyOnChange != undefined) { |
||||
this.notifyOnChange(this.selectionList.formArray.value); |
||||
} |
||||
} |
||||
|
||||
protected addButton() { |
||||
this.selectItems(this.multiSelectFormGroup.value.multiSelect); |
||||
this.multiSelectFormGroup.reset(); |
||||
} |
||||
|
||||
private _itemComparator(a: ApItemViewType, b: ApItemViewType) { |
||||
return ( |
||||
a.type - b.type || |
||||
this.i18nService.collator.compare(a.listName, b.listName) || |
||||
this.i18nService.collator.compare(a.labelName, b.labelName) |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,240 @@
@@ -0,0 +1,240 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended"; |
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; |
||||
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; |
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; |
||||
|
||||
import { AccessPolicySelectorService } from "./access-policy-selector.service"; |
||||
import { ApItemValueType } from "./models/ap-item-value.type"; |
||||
import { ApItemEnum } from "./models/enums/ap-item.enum"; |
||||
import { ApPermissionEnum } from "./models/enums/ap-permission.enum"; |
||||
|
||||
describe("AccessPolicySelectorService", () => { |
||||
let organizationService: MockProxy<OrganizationService>; |
||||
|
||||
let sut: AccessPolicySelectorService; |
||||
|
||||
beforeEach(() => { |
||||
organizationService = mock<OrganizationService>(); |
||||
|
||||
sut = new AccessPolicySelectorService(organizationService); |
||||
}); |
||||
|
||||
afterEach(() => jest.resetAllMocks()); |
||||
|
||||
describe("showAccessRemovalWarning", () => { |
||||
it("returns false when current user is admin", async () => { |
||||
const org = orgFactory(); |
||||
organizationService.get.calledWith(org.id).mockReturnValue(org); |
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = []; |
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); |
||||
|
||||
expect(result).toBe(false); |
||||
}); |
||||
|
||||
it("returns false when current user is owner", async () => { |
||||
const org = orgFactory(); |
||||
org.type = OrganizationUserType.Owner; |
||||
organizationService.get.calledWith(org.id).mockReturnValue(org); |
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = []; |
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); |
||||
|
||||
expect(result).toBe(false); |
||||
}); |
||||
|
||||
it("returns true when current user isn't owner/admin and all policies are removed", async () => { |
||||
const org = setupUserOrg(); |
||||
organizationService.get.calledWith(org.id).mockReturnValue(org); |
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = []; |
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); |
||||
|
||||
expect(result).toBe(true); |
||||
}); |
||||
|
||||
it("returns true when current user isn't owner/admin and user policy is set to canRead", async () => { |
||||
const org = setupUserOrg(); |
||||
organizationService.get.calledWith(org.id).mockReturnValue(org); |
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = []; |
||||
selectedPolicyValues.push( |
||||
createApItemValueType({ |
||||
permission: ApPermissionEnum.CanRead, |
||||
currentUser: true, |
||||
}) |
||||
); |
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); |
||||
|
||||
expect(result).toBe(true); |
||||
}); |
||||
|
||||
it("returns false when current user isn't owner/admin and user policy is set to canReadWrite", async () => { |
||||
const org = setupUserOrg(); |
||||
organizationService.get.calledWith(org.id).mockReturnValue(org); |
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [ |
||||
createApItemValueType({ |
||||
permission: ApPermissionEnum.CanReadWrite, |
||||
currentUser: true, |
||||
}), |
||||
]; |
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); |
||||
|
||||
expect(result).toBe(true); |
||||
}); |
||||
|
||||
it("returns true when current user isn't owner/admin and a group Read policy is submitted that the user is a member of", async () => { |
||||
const org = setupUserOrg(); |
||||
organizationService.get.calledWith(org.id).mockReturnValue(org); |
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [ |
||||
createApItemValueType({ |
||||
id: "groupId", |
||||
type: ApItemEnum.Group, |
||||
permission: ApPermissionEnum.CanRead, |
||||
currentUserInGroup: true, |
||||
}), |
||||
]; |
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); |
||||
|
||||
expect(result).toBe(true); |
||||
}); |
||||
|
||||
it("returns false when current user isn't owner/admin and a group ReadWrite policy is submitted that the user is a member of", async () => { |
||||
const org = setupUserOrg(); |
||||
organizationService.get.calledWith(org.id).mockReturnValue(org); |
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [ |
||||
createApItemValueType({ |
||||
id: "groupId", |
||||
type: ApItemEnum.Group, |
||||
permission: ApPermissionEnum.CanReadWrite, |
||||
currentUserInGroup: true, |
||||
}), |
||||
]; |
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); |
||||
|
||||
expect(result).toBe(false); |
||||
}); |
||||
|
||||
it("returns true when current user isn't owner/admin and a group ReadWrite policy is submitted that the user is not a member of", async () => { |
||||
const org = setupUserOrg(); |
||||
organizationService.get.calledWith(org.id).mockReturnValue(org); |
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [ |
||||
createApItemValueType({ |
||||
id: "groupId", |
||||
type: ApItemEnum.Group, |
||||
permission: ApPermissionEnum.CanReadWrite, |
||||
currentUserInGroup: false, |
||||
}), |
||||
]; |
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); |
||||
|
||||
expect(result).toBe(true); |
||||
}); |
||||
|
||||
it("returns false when current user isn't owner/admin, user policy is set to CanRead, and user is in read write group", async () => { |
||||
const org = setupUserOrg(); |
||||
organizationService.get.calledWith(org.id).mockReturnValue(org); |
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [ |
||||
createApItemValueType({ |
||||
permission: ApPermissionEnum.CanRead, |
||||
currentUser: true, |
||||
}), |
||||
createApItemValueType({ |
||||
id: "groupId", |
||||
type: ApItemEnum.Group, |
||||
permission: ApPermissionEnum.CanReadWrite, |
||||
currentUserInGroup: true, |
||||
}), |
||||
]; |
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); |
||||
|
||||
expect(result).toBe(false); |
||||
}); |
||||
|
||||
it("returns true when current user isn't owner/admin, user policy is set to CanRead, and user is not in ReadWrite group", async () => { |
||||
const org = setupUserOrg(); |
||||
organizationService.get.calledWith(org.id).mockReturnValue(org); |
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [ |
||||
createApItemValueType({ |
||||
permission: ApPermissionEnum.CanRead, |
||||
currentUser: true, |
||||
}), |
||||
createApItemValueType({ |
||||
id: "groupId", |
||||
type: ApItemEnum.Group, |
||||
permission: ApPermissionEnum.CanReadWrite, |
||||
currentUserInGroup: false, |
||||
}), |
||||
]; |
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); |
||||
|
||||
expect(result).toBe(true); |
||||
}); |
||||
|
||||
it("returns true when current user isn't owner/admin, user policy is set to CanRead, and user is in Read group", async () => { |
||||
const org = setupUserOrg(); |
||||
organizationService.get.calledWith(org.id).mockReturnValue(org); |
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [ |
||||
createApItemValueType({ |
||||
permission: ApPermissionEnum.CanRead, |
||||
currentUser: true, |
||||
}), |
||||
createApItemValueType({ |
||||
id: "groupId", |
||||
type: ApItemEnum.Group, |
||||
permission: ApPermissionEnum.CanRead, |
||||
currentUserInGroup: true, |
||||
}), |
||||
]; |
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); |
||||
|
||||
expect(result).toBe(true); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
const orgFactory = (props: Partial<Organization> = {}) => |
||||
Object.assign( |
||||
new Organization(), |
||||
{ |
||||
id: "myOrgId", |
||||
enabled: true, |
||||
type: OrganizationUserType.Admin, |
||||
}, |
||||
props |
||||
); |
||||
|
||||
function createApItemValueType(options: Partial<ApItemValueType> = {}) { |
||||
return { |
||||
id: options?.id ?? "test", |
||||
type: options?.type ?? ApItemEnum.User, |
||||
permission: options?.permission ?? ApPermissionEnum.CanRead, |
||||
currentUserInGroup: options?.currentUserInGroup ?? false, |
||||
}; |
||||
} |
||||
|
||||
function setupUserOrg() { |
||||
const userId = "testUserId"; |
||||
const org = orgFactory({ userId: userId }); |
||||
org.type = OrganizationUserType.User; |
||||
return org; |
||||
} |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
import { Injectable } from "@angular/core"; |
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; |
||||
|
||||
import { ApItemValueType } from "./models/ap-item-value.type"; |
||||
import { ApItemEnum } from "./models/enums/ap-item.enum"; |
||||
import { ApPermissionEnum } from "./models/enums/ap-permission.enum"; |
||||
|
||||
@Injectable({ |
||||
providedIn: "root", |
||||
}) |
||||
export class AccessPolicySelectorService { |
||||
constructor(private organizationService: OrganizationService) {} |
||||
|
||||
async showAccessRemovalWarning( |
||||
organizationId: string, |
||||
selectedPoliciesValues: ApItemValueType[] |
||||
): Promise<boolean> { |
||||
const organization = this.organizationService.get(organizationId); |
||||
if (organization.isOwner || organization.isAdmin) { |
||||
return false; |
||||
} |
||||
|
||||
const selectedUserReadWritePolicy = selectedPoliciesValues.find( |
||||
(s) => |
||||
s.type === ApItemEnum.User && |
||||
s.currentUser && |
||||
s.permission === ApPermissionEnum.CanReadWrite |
||||
); |
||||
|
||||
const selectedGroupReadWritePolicies = selectedPoliciesValues.filter( |
||||
(s) => |
||||
s.type === ApItemEnum.Group && |
||||
s.permission == ApPermissionEnum.CanReadWrite && |
||||
s.currentUserInGroup |
||||
); |
||||
|
||||
if (selectedGroupReadWritePolicies == null || selectedGroupReadWritePolicies.length == 0) { |
||||
if (selectedUserReadWritePolicy == null) { |
||||
return true; |
||||
} else { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
} |
||||
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
import { |
||||
ProjectPeopleAccessPoliciesView, |
||||
UserProjectAccessPolicyView, |
||||
GroupProjectAccessPolicyView, |
||||
} from "../../../../models/view/access-policy.view"; |
||||
|
||||
import { ApItemEnum } from "./enums/ap-item.enum"; |
||||
import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.enum"; |
||||
|
||||
export type ApItemValueType = { |
||||
id: string; |
||||
type: ApItemEnum; |
||||
permission: ApPermissionEnum; |
||||
currentUserInGroup?: boolean; |
||||
currentUser?: boolean; |
||||
}; |
||||
|
||||
export function convertToProjectPeopleAccessPoliciesView( |
||||
projectId: string, |
||||
selectedPolicyValues: ApItemValueType[] |
||||
): ProjectPeopleAccessPoliciesView { |
||||
const view = new ProjectPeopleAccessPoliciesView(); |
||||
view.userAccessPolicies = selectedPolicyValues |
||||
.filter((x) => x.type == ApItemEnum.User) |
||||
.map((filtered) => { |
||||
const policyView = new UserProjectAccessPolicyView(); |
||||
policyView.grantedProjectId = projectId; |
||||
policyView.organizationUserId = filtered.id; |
||||
policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); |
||||
policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); |
||||
return policyView; |
||||
}); |
||||
|
||||
view.groupAccessPolicies = selectedPolicyValues |
||||
.filter((x) => x.type == ApItemEnum.Group) |
||||
.map((filtered) => { |
||||
const policyView = new GroupProjectAccessPolicyView(); |
||||
policyView.grantedProjectId = projectId; |
||||
policyView.groupId = filtered.id; |
||||
policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); |
||||
policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); |
||||
return policyView; |
||||
}); |
||||
return view; |
||||
} |
||||
@ -0,0 +1,111 @@
@@ -0,0 +1,111 @@
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils"; |
||||
import { SelectItemView } from "@bitwarden/components"; |
||||
|
||||
import { ProjectPeopleAccessPoliciesView } from "../../../../models/view/access-policy.view"; |
||||
import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view"; |
||||
|
||||
import { ApItemEnum, ApItemEnumUtil } from "./enums/ap-item.enum"; |
||||
import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.enum"; |
||||
|
||||
export type ApItemViewType = |
||||
| SelectItemView & { |
||||
accessPolicyId?: string; |
||||
permission?: ApPermissionEnum; |
||||
} & ( |
||||
| { |
||||
type: ApItemEnum.User; |
||||
userId?: string; |
||||
currentUser?: boolean; |
||||
} |
||||
| { |
||||
type: ApItemEnum.Group; |
||||
currentUserInGroup?: boolean; |
||||
} |
||||
| { |
||||
type: ApItemEnum.ServiceAccount; |
||||
} |
||||
| { |
||||
type: ApItemEnum.Project; |
||||
} |
||||
); |
||||
|
||||
export function convertToAccessPolicyItemViews( |
||||
value: ProjectPeopleAccessPoliciesView |
||||
): ApItemViewType[] { |
||||
const accessPolicies: ApItemViewType[] = []; |
||||
|
||||
value.userAccessPolicies.forEach((policy) => { |
||||
accessPolicies.push({ |
||||
type: ApItemEnum.User, |
||||
icon: ApItemEnumUtil.itemIcon(ApItemEnum.User), |
||||
id: policy.organizationUserId, |
||||
accessPolicyId: policy.id, |
||||
labelName: policy.organizationUserName, |
||||
listName: policy.organizationUserName, |
||||
permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write), |
||||
userId: policy.userId, |
||||
currentUser: policy.currentUser, |
||||
}); |
||||
}); |
||||
|
||||
value.groupAccessPolicies.forEach((policy) => { |
||||
accessPolicies.push({ |
||||
type: ApItemEnum.Group, |
||||
icon: ApItemEnumUtil.itemIcon(ApItemEnum.Group), |
||||
id: policy.groupId, |
||||
accessPolicyId: policy.id, |
||||
labelName: policy.groupName, |
||||
listName: policy.groupName, |
||||
permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write), |
||||
currentUserInGroup: policy.currentUserInGroup, |
||||
}); |
||||
}); |
||||
|
||||
return accessPolicies; |
||||
} |
||||
|
||||
export function convertPotentialGranteesToApItemViewType( |
||||
grantees: PotentialGranteeView[] |
||||
): ApItemViewType[] { |
||||
return grantees.map((granteeView) => { |
||||
let icon: string; |
||||
let type: ApItemEnum; |
||||
let listName = granteeView.name; |
||||
let labelName = granteeView.name; |
||||
|
||||
switch (granteeView.type) { |
||||
case "user": |
||||
icon = ApItemEnumUtil.itemIcon(ApItemEnum.User); |
||||
type = ApItemEnum.User; |
||||
if (Utils.isNullOrWhitespace(granteeView.name)) { |
||||
listName = granteeView.email; |
||||
labelName = granteeView.email; |
||||
} else { |
||||
listName = `${granteeView.name} (${granteeView.email})`; |
||||
} |
||||
break; |
||||
case "group": |
||||
icon = ApItemEnumUtil.itemIcon(ApItemEnum.Group); |
||||
type = ApItemEnum.Group; |
||||
break; |
||||
case "serviceAccount": |
||||
icon = ApItemEnumUtil.itemIcon(ApItemEnum.ServiceAccount); |
||||
type = ApItemEnum.ServiceAccount; |
||||
break; |
||||
case "project": |
||||
icon = ApItemEnumUtil.itemIcon(ApItemEnum.Project); |
||||
type = ApItemEnum.Project; |
||||
break; |
||||
} |
||||
|
||||
return { |
||||
icon: icon, |
||||
type: type, |
||||
id: granteeView.id, |
||||
labelName: labelName, |
||||
listName: listName, |
||||
currentUserInGroup: granteeView.currentUserInGroup, |
||||
currentUser: granteeView.currentUser, |
||||
}; |
||||
}); |
||||
} |
||||
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
export enum ApItemEnum { |
||||
User, |
||||
Group, |
||||
ServiceAccount, |
||||
Project, |
||||
} |
||||
|
||||
export class ApItemEnumUtil { |
||||
static itemIcon(type: ApItemEnum): string { |
||||
switch (type) { |
||||
case ApItemEnum.User: |
||||
return "bwi-user"; |
||||
case ApItemEnum.Group: |
||||
return "bwi-family"; |
||||
case ApItemEnum.ServiceAccount: |
||||
return "bwi-wrench"; |
||||
case ApItemEnum.Project: |
||||
return "bwi-collection"; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
export enum ApPermissionEnum { |
||||
CanRead = "canRead", |
||||
CanReadWrite = "canReadWrite", |
||||
} |
||||
|
||||
export class ApPermissionEnumUtil { |
||||
static toApPermissionEnum(read: boolean, write: boolean): ApPermissionEnum { |
||||
if (read && write) { |
||||
return ApPermissionEnum.CanReadWrite; |
||||
} else if (read) { |
||||
return ApPermissionEnum.CanRead; |
||||
} else { |
||||
throw new Error("Unsupported Access Policy Permission option"); |
||||
} |
||||
} |
||||
|
||||
static toRead(permission: ApPermissionEnum): boolean { |
||||
if (permission == ApPermissionEnum.CanRead || permission == ApPermissionEnum.CanReadWrite) { |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
static toWrite(permission: ApPermissionEnum): boolean { |
||||
if (permission === ApPermissionEnum.CanReadWrite) { |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
import { AccessPolicyRequest } from "./access-policy.request"; |
||||
|
||||
export class PeopleAccessPoliciesRequest { |
||||
userAccessPolicyRequests?: AccessPolicyRequest[]; |
||||
groupAccessPolicyRequests?: AccessPolicyRequest[]; |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response"; |
||||
|
||||
import { |
||||
GroupProjectAccessPolicyResponse, |
||||
UserProjectAccessPolicyResponse, |
||||
} from "./access-policy.response"; |
||||
|
||||
export class ProjectPeopleAccessPoliciesResponse extends BaseResponse { |
||||
userAccessPolicies: UserProjectAccessPolicyResponse[]; |
||||
groupAccessPolicies: GroupProjectAccessPolicyResponse[]; |
||||
|
||||
constructor(response: any) { |
||||
super(response); |
||||
const userAccessPolicies = this.getResponseProperty("UserAccessPolicies"); |
||||
this.userAccessPolicies = userAccessPolicies.map( |
||||
(k: any) => new UserProjectAccessPolicyResponse(k) |
||||
); |
||||
const groupAccessPolicies = this.getResponseProperty("GroupAccessPolicies"); |
||||
this.groupAccessPolicies = groupAccessPolicies.map( |
||||
(k: any) => new GroupProjectAccessPolicyResponse(k) |
||||
); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue