Browse Source
* Initial working multi select * Create project access selector component * Refactor to shared components for access policies * Refactor access policies table * Initial working create & update access policies * Add dynamic multi-select + DRY refactor * FIGMA updates * Fix table and refactor access-policy service * Code review updates * Fix disable/loading logic for selector * Don't run onchange for creation * Migrate to new group service * simplify async action * Refactor access-policies Co-authored-by: Will Martin <willmartian@users.noreply.github.com> * Refactor access-selector * Add using potential grantee endpoints. * refactor to use observables * combine access-selector and access-policies component * lift dynamic i18n out of template * use strict equality * fix multiselect refresh * change grantees to function * Fix multiple HTTP calls * don't broadcast on AP update * Code review updates * Use refactored potential-grantees endpoint * potential grantees refactor v2 --------- Co-authored-by: Will Martin <willmartian@users.noreply.github.com> Co-authored-by: William Martin <contact@willmartian.com>pull/4676/head
23 changed files with 939 additions and 3 deletions
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
export class BaseAccessPolicyView { |
||||
id: string; |
||||
read: boolean; |
||||
write: boolean; |
||||
creationDate: string; |
||||
revisionDate: string; |
||||
} |
||||
|
||||
export class UserProjectAccessPolicyView extends BaseAccessPolicyView { |
||||
organizationUserId: string; |
||||
organizationUserName: string; |
||||
grantedProjectId: string; |
||||
} |
||||
|
||||
export class GroupProjectAccessPolicyView extends BaseAccessPolicyView { |
||||
groupId: string; |
||||
groupName: string; |
||||
grantedProjectId: string; |
||||
} |
||||
|
||||
export class ServiceAccountProjectAccessPolicyView extends BaseAccessPolicyView { |
||||
serviceAccountId: string; |
||||
serviceAccountName: string; |
||||
grantedProjectId: string; |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
export class PotentialGranteeView { |
||||
id: string; |
||||
name: string; |
||||
type: string; |
||||
email: string; |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
import { |
||||
GroupProjectAccessPolicyView, |
||||
ServiceAccountProjectAccessPolicyView, |
||||
UserProjectAccessPolicyView, |
||||
} from "./access-policy.view"; |
||||
|
||||
export class ProjectAccessPoliciesView { |
||||
userAccessPolicies: UserProjectAccessPolicyView[]; |
||||
groupAccessPolicies: GroupProjectAccessPolicyView[]; |
||||
serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[]; |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
<ng-container *ngIf="projectAccessPolicies$; else spinner"> |
||||
<div class="tw-w-2/5"> |
||||
<p class="tw-mt-8"> |
||||
{{ description }} |
||||
</p> |
||||
<sm-access-selector |
||||
[projectAccessPolicies$]="projectAccessPolicies$" |
||||
[potentialGrantees$]="potentialGrantees$" |
||||
[label]="label" |
||||
[hint]="hint" |
||||
[tableType]="accessType" |
||||
[columnTitle]="columnTitle" |
||||
[emptyMessage]="emptyMessage" |
||||
> |
||||
</sm-access-selector> |
||||
</div> |
||||
</ng-container> |
||||
|
||||
<ng-template #spinner> |
||||
<div class="tw-items-center tw-justify-center tw-pt-64 tw-text-center"> |
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i> |
||||
</div> |
||||
</ng-template> |
||||
@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
|
||||
import { Component, Input, OnInit } from "@angular/core"; |
||||
import { ActivatedRoute } from "@angular/router"; |
||||
import { combineLatestWith, Observable, share, startWith, switchMap } from "rxjs"; |
||||
|
||||
import { PotentialGranteeView } from "../../models/view/potential-grantee.view"; |
||||
import { ProjectAccessPoliciesView } from "../../models/view/project-access-policies.view"; |
||||
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service"; |
||||
|
||||
@Component({ |
||||
selector: "sm-project-access", |
||||
templateUrl: "./project-access.component.html", |
||||
}) |
||||
export class ProjectAccessComponent implements OnInit { |
||||
@Input() accessType: "projectPeople" | "projectServiceAccounts"; |
||||
@Input() description: string; |
||||
@Input() label: string; |
||||
@Input() hint: string; |
||||
@Input() columnTitle: string; |
||||
@Input() emptyMessage: string; |
||||
|
||||
protected projectAccessPolicies$: Observable<ProjectAccessPoliciesView>; |
||||
protected potentialGrantees$: Observable<PotentialGranteeView[]>; |
||||
|
||||
constructor(private route: ActivatedRoute, private accessPolicyService: AccessPolicyService) {} |
||||
|
||||
ngOnInit(): void { |
||||
this.projectAccessPolicies$ = this.accessPolicyService.projectAccessPolicies$.pipe( |
||||
startWith(null), |
||||
combineLatestWith(this.route.params), |
||||
switchMap(([_, params]) => { |
||||
return this.accessPolicyService.getProjectAccessPolicies( |
||||
params.organizationId, |
||||
params.projectId |
||||
); |
||||
}), |
||||
share() |
||||
); |
||||
|
||||
this.potentialGrantees$ = this.accessPolicyService.projectAccessPolicies$.pipe( |
||||
startWith(null), |
||||
combineLatestWith(this.route.params), |
||||
switchMap(async ([_, params]) => { |
||||
if (this.accessType == "projectPeople") { |
||||
return await this.accessPolicyService.getPeoplePotentialGrantees(params.organizationId); |
||||
} else { |
||||
return await this.accessPolicyService.getServiceAccountsPotentialGrantees( |
||||
params.organizationId |
||||
); |
||||
} |
||||
}), |
||||
share() |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
<sm-project-access |
||||
accessType="projectPeople" |
||||
[description]="'projectPeopleDescription' | i18n" |
||||
[label]="'people' | i18n" |
||||
[hint]="'projectPeopleSelectHint' | i18n" |
||||
[columnTitle]="'groupSlashUser' | i18n" |
||||
[emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n" |
||||
> |
||||
</sm-project-access> |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
import { Component } from "@angular/core"; |
||||
|
||||
@Component({ |
||||
selector: "sm-project-people", |
||||
templateUrl: "./project-people.component.html", |
||||
}) |
||||
export class ProjectPeopleComponent {} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
<sm-project-access |
||||
accessType="projectServiceAccounts" |
||||
[description]="'projectServiceAccountsDescription' | i18n" |
||||
[label]="'serviceAccounts' | i18n" |
||||
[hint]="'projectServiceAccountsSelectHint' | i18n" |
||||
[columnTitle]="'serviceAccounts' | i18n" |
||||
[emptyMessage]="'projectEmptyServiceAccountAccessPolicies' | i18n" |
||||
> |
||||
</sm-project-access> |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
import { Component } from "@angular/core"; |
||||
|
||||
@Component({ |
||||
selector: "sm-project-service-accounts", |
||||
templateUrl: "./project-service-accounts.component.html", |
||||
}) |
||||
export class ProjectServiceAccountsComponent {} |
||||
@ -0,0 +1,260 @@
@@ -0,0 +1,260 @@
|
||||
import { Injectable } from "@angular/core"; |
||||
import { Subject } from "rxjs"; |
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service"; |
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; |
||||
import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service"; |
||||
import { EncString } from "@bitwarden/common/models/domain/enc-string"; |
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key"; |
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response"; |
||||
|
||||
import { |
||||
BaseAccessPolicyView, |
||||
GroupProjectAccessPolicyView, |
||||
ServiceAccountProjectAccessPolicyView, |
||||
UserProjectAccessPolicyView, |
||||
} from "../../models/view/access-policy.view"; |
||||
import { PotentialGranteeView } from "../../models/view/potential-grantee.view"; |
||||
import { ProjectAccessPoliciesView } from "../../models/view/project-access-policies.view"; |
||||
|
||||
import { AccessPoliciesCreateRequest } from "./models/requests/access-policies-create.request"; |
||||
import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request"; |
||||
import { AccessPolicyRequest } from "./models/requests/access-policy.request"; |
||||
import { |
||||
GroupProjectAccessPolicyResponse, |
||||
ServiceAccountProjectAccessPolicyResponse, |
||||
UserProjectAccessPolicyResponse, |
||||
} from "./models/responses/access-policy.response"; |
||||
import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response"; |
||||
import { ProjectAccessPoliciesResponse } from "./models/responses/project-access-policies.response"; |
||||
|
||||
@Injectable({ |
||||
providedIn: "root", |
||||
}) |
||||
export class AccessPolicyService { |
||||
protected _projectAccessPolicies = new Subject<ProjectAccessPoliciesView>(); |
||||
projectAccessPolicies$ = this._projectAccessPolicies.asObservable(); |
||||
|
||||
constructor( |
||||
private cryptoService: CryptoService, |
||||
private apiService: ApiService, |
||||
private encryptService: EncryptService |
||||
) {} |
||||
|
||||
async getProjectAccessPolicies( |
||||
organizationId: string, |
||||
projectId: string |
||||
): Promise<ProjectAccessPoliciesView> { |
||||
const r = await this.apiService.send( |
||||
"GET", |
||||
"/projects/" + projectId + "/access-policies", |
||||
null, |
||||
true, |
||||
true |
||||
); |
||||
|
||||
const results = new ProjectAccessPoliciesResponse(r); |
||||
return await this.createProjectAccessPoliciesView(organizationId, results); |
||||
} |
||||
|
||||
async getPeoplePotentialGrantees(organizationId: string) { |
||||
const r = await this.apiService.send( |
||||
"GET", |
||||
"/organizations/" + organizationId + "/access-policies/people/potential-grantees", |
||||
null, |
||||
true, |
||||
true |
||||
); |
||||
const results = new ListResponse(r, PotentialGranteeResponse); |
||||
return await this.createPotentialGranteeViews(organizationId, results.data); |
||||
} |
||||
|
||||
async getServiceAccountsPotentialGrantees(organizationId: string) { |
||||
const r = await this.apiService.send( |
||||
"GET", |
||||
"/organizations/" + organizationId + "/access-policies/service-accounts/potential-grantees", |
||||
null, |
||||
true, |
||||
true |
||||
); |
||||
const results = new ListResponse(r, PotentialGranteeResponse); |
||||
return await this.createPotentialGranteeViews(organizationId, results.data); |
||||
} |
||||
|
||||
async deleteAccessPolicy(accessPolicyId: string): Promise<void> { |
||||
await this.apiService.send("DELETE", "/access-policies/" + accessPolicyId, null, true, false); |
||||
this._projectAccessPolicies.next(null); |
||||
} |
||||
|
||||
async updateAccessPolicy(baseAccessPolicyView: BaseAccessPolicyView): Promise<void> { |
||||
const payload = new AccessPolicyUpdateRequest(); |
||||
payload.read = baseAccessPolicyView.read; |
||||
payload.write = baseAccessPolicyView.write; |
||||
await this.apiService.send( |
||||
"PUT", |
||||
"/access-policies/" + baseAccessPolicyView.id, |
||||
payload, |
||||
true, |
||||
true |
||||
); |
||||
} |
||||
|
||||
async createProjectAccessPolicies( |
||||
organizationId: string, |
||||
projectId: string, |
||||
projectAccessPoliciesView: ProjectAccessPoliciesView |
||||
): Promise<ProjectAccessPoliciesView> { |
||||
const request = this.getAccessPoliciesCreateRequest(projectAccessPoliciesView); |
||||
const r = await this.apiService.send( |
||||
"POST", |
||||
"/projects/" + projectId + "/access-policies", |
||||
request, |
||||
true, |
||||
true |
||||
); |
||||
const results = new ProjectAccessPoliciesResponse(r); |
||||
const view = await this.createProjectAccessPoliciesView(organizationId, results); |
||||
this._projectAccessPolicies.next(view); |
||||
return view; |
||||
} |
||||
|
||||
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> { |
||||
return await this.cryptoService.getOrgKey(organizationId); |
||||
} |
||||
|
||||
private getAccessPoliciesCreateRequest( |
||||
projectAccessPoliciesView: ProjectAccessPoliciesView |
||||
): AccessPoliciesCreateRequest { |
||||
const createRequest = new AccessPoliciesCreateRequest(); |
||||
|
||||
if (projectAccessPoliciesView.userAccessPolicies?.length > 0) { |
||||
createRequest.userAccessPolicyRequests = projectAccessPoliciesView.userAccessPolicies.map( |
||||
(ap) => { |
||||
return this.getAccessPolicyRequest(ap.organizationUserId, ap); |
||||
} |
||||
); |
||||
} |
||||
|
||||
if (projectAccessPoliciesView.groupAccessPolicies?.length > 0) { |
||||
createRequest.groupAccessPolicyRequests = projectAccessPoliciesView.groupAccessPolicies.map( |
||||
(ap) => { |
||||
return this.getAccessPolicyRequest(ap.groupId, ap); |
||||
} |
||||
); |
||||
} |
||||
|
||||
if (projectAccessPoliciesView.serviceAccountAccessPolicies?.length > 0) { |
||||
createRequest.serviceAccountAccessPolicyRequests = |
||||
projectAccessPoliciesView.serviceAccountAccessPolicies.map((ap) => { |
||||
return this.getAccessPolicyRequest(ap.serviceAccountId, ap); |
||||
}); |
||||
} |
||||
return createRequest; |
||||
} |
||||
|
||||
private getAccessPolicyRequest( |
||||
granteeId: string, |
||||
view: |
||||
| UserProjectAccessPolicyView |
||||
| GroupProjectAccessPolicyView |
||||
| ServiceAccountProjectAccessPolicyView |
||||
) { |
||||
const request = new AccessPolicyRequest(); |
||||
request.granteeId = granteeId; |
||||
request.read = view.read; |
||||
request.write = view.write; |
||||
return request; |
||||
} |
||||
|
||||
private async createProjectAccessPoliciesView( |
||||
organizationId: string, |
||||
projectAccessPoliciesResponse: ProjectAccessPoliciesResponse |
||||
): Promise<ProjectAccessPoliciesView> { |
||||
const orgKey = await this.getOrganizationKey(organizationId); |
||||
const view = new ProjectAccessPoliciesView(); |
||||
|
||||
view.userAccessPolicies = projectAccessPoliciesResponse.userAccessPolicies.map((ap) => { |
||||
return this.createUserProjectAccessPolicyView(ap); |
||||
}); |
||||
view.groupAccessPolicies = projectAccessPoliciesResponse.groupAccessPolicies.map((ap) => { |
||||
return this.createGroupProjectAccessPolicyView(ap); |
||||
}); |
||||
view.serviceAccountAccessPolicies = await Promise.all( |
||||
projectAccessPoliciesResponse.serviceAccountAccessPolicies.map(async (ap) => { |
||||
return await this.createServiceAccountProjectAccessPolicyView(orgKey, ap); |
||||
}) |
||||
); |
||||
return view; |
||||
} |
||||
|
||||
private createUserProjectAccessPolicyView( |
||||
response: UserProjectAccessPolicyResponse |
||||
): UserProjectAccessPolicyView { |
||||
const view = <UserProjectAccessPolicyView>this.createBaseAccessPolicyView(response); |
||||
view.grantedProjectId = response.grantedProjectId; |
||||
view.organizationUserId = response.organizationUserId; |
||||
view.organizationUserName = response.organizationUserName; |
||||
return view; |
||||
} |
||||
|
||||
private createGroupProjectAccessPolicyView( |
||||
response: GroupProjectAccessPolicyResponse |
||||
): GroupProjectAccessPolicyView { |
||||
const view = <GroupProjectAccessPolicyView>this.createBaseAccessPolicyView(response); |
||||
view.grantedProjectId = response.grantedProjectId; |
||||
view.groupId = response.groupId; |
||||
view.groupName = response.groupName; |
||||
return view; |
||||
} |
||||
|
||||
private async createServiceAccountProjectAccessPolicyView( |
||||
organizationKey: SymmetricCryptoKey, |
||||
response: ServiceAccountProjectAccessPolicyResponse |
||||
): Promise<ServiceAccountProjectAccessPolicyView> { |
||||
const view = <ServiceAccountProjectAccessPolicyView>this.createBaseAccessPolicyView(response); |
||||
view.grantedProjectId = response.grantedProjectId; |
||||
view.serviceAccountId = response.serviceAccountId; |
||||
view.serviceAccountName = await this.encryptService.decryptToUtf8( |
||||
new EncString(response.serviceAccountName), |
||||
organizationKey |
||||
); |
||||
return view; |
||||
} |
||||
|
||||
private createBaseAccessPolicyView( |
||||
response: |
||||
| UserProjectAccessPolicyResponse |
||||
| GroupProjectAccessPolicyResponse |
||||
| ServiceAccountProjectAccessPolicyResponse |
||||
) { |
||||
return { |
||||
id: response.id, |
||||
read: response.read, |
||||
write: response.write, |
||||
creationDate: response.creationDate, |
||||
revisionDate: response.revisionDate, |
||||
}; |
||||
} |
||||
|
||||
private async createPotentialGranteeViews( |
||||
organizationId: string, |
||||
results: PotentialGranteeResponse[] |
||||
): Promise<PotentialGranteeView[]> { |
||||
const orgKey = await this.getOrganizationKey(organizationId); |
||||
return await Promise.all( |
||||
results.map(async (r) => { |
||||
const view = new PotentialGranteeView(); |
||||
view.id = r.id; |
||||
view.type = r.type; |
||||
view.email = r.email; |
||||
|
||||
if (r.type === "serviceAccount") { |
||||
view.name = await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey); |
||||
} else { |
||||
view.name = r.name; |
||||
} |
||||
return view; |
||||
}) |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit" class="tw-mt-5"> |
||||
<bit-form-field> |
||||
<bit-label>{{ label }}</bit-label> |
||||
<bit-multi-select |
||||
class="tw-mr-4 tw-w-full" |
||||
formControlName="multiSelect" |
||||
[baseItems]="selectItemsView$ | async" |
||||
[loading]="loading" |
||||
></bit-multi-select> |
||||
<bit-hint>{{ hint }}</bit-hint> |
||||
<button type="submit" bitButton buttonType="primary" bitFormButton> |
||||
{{ "add" | i18n }} |
||||
</button> |
||||
</bit-form-field> |
||||
</form> |
||||
|
||||
<ng-container *ngIf="rows$ | async as rows; else spinner"> |
||||
<bit-table> |
||||
<ng-container header> |
||||
<tr> |
||||
<th bitCell colspan="2">{{ columnTitle }}</th> |
||||
<th bitCell>{{ "permissions" | i18n }}</th> |
||||
</tr> |
||||
</ng-container> |
||||
|
||||
<ng-template body> |
||||
<ng-container *ngIf="rows?.length > 0; else empty"> |
||||
<tr bitRow *ngFor="let row of rows"> |
||||
<td bitCell class="tw-w-0 tw-pr-0"> |
||||
<i class="bwi {{ row.icon }} tw-text-xl tw-text-muted" aria-hidden="true"></i> |
||||
</td> |
||||
<td bitCell>{{ row.name }}</td> |
||||
<td bitCell *ngIf="row.type == 'serviceAccount'"> |
||||
<bit-form-field class="tw-inline-block tw-w-auto"> |
||||
<select bitInput disabled> |
||||
<option value="canRead" selected>{{ "canRead" | i18n }}</option> |
||||
</select> |
||||
</bit-form-field> |
||||
</td> |
||||
<td bitCell *ngIf="row.type != 'serviceAccount'"> |
||||
<bit-form-field class="tw-inline-block tw-w-auto"> |
||||
<select bitInput (change)="updateAccessPolicy($event.target, row.id)"> |
||||
<option value="canRead" [selected]="row.read && row.write != true"> |
||||
{{ "canRead" | i18n }} |
||||
</option> |
||||
<option value="canWrite" [selected]="row.read != true && row.write"> |
||||
{{ "canWrite" | i18n }} |
||||
</option> |
||||
<option value="canReadWrite" [selected]="row.read && row.write"> |
||||
{{ "canReadWrite" | i18n }} |
||||
</option> |
||||
</select> |
||||
</bit-form-field> |
||||
</td> |
||||
<td bitCell class="tw-w-0"> |
||||
<button |
||||
type="button" |
||||
bitIconButton="bwi-close" |
||||
buttonType="main" |
||||
size="default" |
||||
[attr.title]="'close' | i18n" |
||||
[attr.aria-label]="'close' | i18n" |
||||
[bitAction]="delete(row.id)" |
||||
></button> |
||||
</td> |
||||
</tr> |
||||
</ng-container> |
||||
</ng-template> |
||||
</bit-table> |
||||
</ng-container> |
||||
|
||||
<ng-template #empty> |
||||
<div class="tw-mt-4 tw-text-center"> |
||||
{{ emptyMessage }} |
||||
</div> |
||||
</ng-template> |
||||
|
||||
<ng-template #spinner> |
||||
<div class="tw-items-center tw-justify-center tw-pt-64 tw-text-center"> |
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i> |
||||
</div> |
||||
</ng-template> |
||||
@ -0,0 +1,286 @@
@@ -0,0 +1,286 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from "@angular/core"; |
||||
import { FormControl, FormGroup, Validators } from "@angular/forms"; |
||||
import { ActivatedRoute } from "@angular/router"; |
||||
import { |
||||
combineLatestWith, |
||||
distinctUntilChanged, |
||||
firstValueFrom, |
||||
map, |
||||
Observable, |
||||
Subject, |
||||
takeUntil, |
||||
tap, |
||||
} from "rxjs"; |
||||
|
||||
import { ValidationService } from "@bitwarden/common/abstractions/validation.service"; |
||||
import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view"; |
||||
|
||||
import { |
||||
BaseAccessPolicyView, |
||||
GroupProjectAccessPolicyView, |
||||
ServiceAccountProjectAccessPolicyView, |
||||
UserProjectAccessPolicyView, |
||||
} from "../../models/view/access-policy.view"; |
||||
import { PotentialGranteeView } from "../../models/view/potential-grantee.view"; |
||||
import { ProjectAccessPoliciesView } from "../../models/view/project-access-policies.view"; |
||||
|
||||
import { AccessPolicyService } from "./access-policy.service"; |
||||
|
||||
type RowItemView = { |
||||
type: "user" | "group" | "serviceAccount"; |
||||
name: string; |
||||
id: string; |
||||
read: boolean; |
||||
write: boolean; |
||||
icon: string; |
||||
}; |
||||
|
||||
@Component({ |
||||
selector: "sm-access-selector", |
||||
templateUrl: "./access-selector.component.html", |
||||
}) |
||||
export class AccessSelectorComponent implements OnInit, OnDestroy { |
||||
@Input() label: string; |
||||
@Input() hint: string; |
||||
@Input() tableType: "projectPeople" | "projectServiceAccounts"; |
||||
@Input() columnTitle: string; |
||||
@Input() emptyMessage: string; |
||||
|
||||
private readonly userIcon = "bwi-user"; |
||||
private readonly groupIcon = "bwi-family"; |
||||
private readonly serviceAccountIcon = "bwi-wrench"; |
||||
|
||||
@Input() projectAccessPolicies$: Observable<ProjectAccessPoliciesView>; |
||||
@Input() potentialGrantees$: Observable<PotentialGranteeView[]>; |
||||
|
||||
private projectId: string; |
||||
private organizationId: string; |
||||
private destroy$: Subject<void> = new Subject<void>(); |
||||
|
||||
protected loading = true; |
||||
protected formGroup = new FormGroup({ |
||||
multiSelect: new FormControl([], [Validators.required]), |
||||
}); |
||||
|
||||
protected selectItemsView$: Observable<SelectItemView[]>; |
||||
protected rows$: Observable<RowItemView[]>; |
||||
|
||||
constructor( |
||||
private route: ActivatedRoute, |
||||
private accessPolicyService: AccessPolicyService, |
||||
private validationService: ValidationService |
||||
) {} |
||||
|
||||
async ngOnInit(): Promise<void> { |
||||
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params: any) => { |
||||
this.organizationId = params.organizationId; |
||||
this.projectId = params.projectId; |
||||
}); |
||||
|
||||
this.selectItemsView$ = this.projectAccessPolicies$.pipe( |
||||
distinctUntilChanged( |
||||
(prev, curr) => this.getAccessPoliciesCount(curr) === this.getAccessPoliciesCount(prev) |
||||
), |
||||
combineLatestWith(this.potentialGrantees$), |
||||
map(([projectAccessPolicies, potentialGrantees]) => |
||||
this.createSelectView(projectAccessPolicies, potentialGrantees) |
||||
), |
||||
tap(() => { |
||||
this.loading = false; |
||||
this.formGroup.enable(); |
||||
this.formGroup.reset(); |
||||
}) |
||||
); |
||||
|
||||
this.rows$ = this.projectAccessPolicies$.pipe( |
||||
map((policies) => { |
||||
const rowData: RowItemView[] = []; |
||||
|
||||
if (this.tableType === "projectPeople") { |
||||
policies.userAccessPolicies.forEach((policy) => { |
||||
rowData.push({ |
||||
type: "user", |
||||
name: policy.organizationUserName, |
||||
id: policy.id, |
||||
read: policy.read, |
||||
write: policy.write, |
||||
icon: this.userIcon, |
||||
}); |
||||
}); |
||||
|
||||
policies.groupAccessPolicies.forEach((policy) => { |
||||
rowData.push({ |
||||
type: "group", |
||||
name: policy.groupName, |
||||
id: policy.id, |
||||
read: policy.read, |
||||
write: policy.write, |
||||
icon: this.groupIcon, |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
if (this.tableType === "projectServiceAccounts") { |
||||
policies.serviceAccountAccessPolicies.forEach((policy) => { |
||||
rowData.push({ |
||||
type: "serviceAccount", |
||||
name: policy.serviceAccountName, |
||||
id: policy.id, |
||||
read: policy.read, |
||||
write: policy.write, |
||||
icon: this.serviceAccountIcon, |
||||
}); |
||||
}); |
||||
} |
||||
return rowData; |
||||
}) |
||||
); |
||||
} |
||||
|
||||
ngOnDestroy(): void { |
||||
this.destroy$.next(); |
||||
this.destroy$.complete(); |
||||
} |
||||
|
||||
submit = async () => { |
||||
this.formGroup.markAllAsTouched(); |
||||
if (this.formGroup.invalid) { |
||||
return; |
||||
} |
||||
this.loading = true; |
||||
this.formGroup.disable(); |
||||
|
||||
this.accessPolicyService.createProjectAccessPolicies( |
||||
this.organizationId, |
||||
this.projectId, |
||||
this.createProjectAccessPoliciesViewFromSelected() |
||||
); |
||||
|
||||
return firstValueFrom(this.selectItemsView$); |
||||
}; |
||||
|
||||
private createSelectView = ( |
||||
projectAccessPolicies: ProjectAccessPoliciesView, |
||||
potentialGrantees: PotentialGranteeView[] |
||||
): SelectItemView[] => { |
||||
const selectItemsView = potentialGrantees.map((granteeView) => { |
||||
let icon: string; |
||||
let listName: string; |
||||
if (granteeView.type === "user") { |
||||
icon = this.userIcon; |
||||
listName = `${granteeView.name} (${granteeView.email})`; |
||||
} else if (granteeView.type === "group") { |
||||
icon = this.groupIcon; |
||||
listName = granteeView.name; |
||||
} else { |
||||
icon = this.serviceAccountIcon; |
||||
listName = granteeView.name; |
||||
} |
||||
return { |
||||
icon: icon, |
||||
id: granteeView.id, |
||||
labelName: granteeView.name, |
||||
listName: listName, |
||||
}; |
||||
}); |
||||
return this.filterExistingAccessPolicies(selectItemsView, projectAccessPolicies); |
||||
}; |
||||
|
||||
private createProjectAccessPoliciesViewFromSelected(): ProjectAccessPoliciesView { |
||||
const projectAccessPoliciesView = new ProjectAccessPoliciesView(); |
||||
projectAccessPoliciesView.userAccessPolicies = this.formGroup.value.multiSelect |
||||
?.filter((selection) => selection.icon === this.userIcon) |
||||
?.map((filtered) => { |
||||
const view = new UserProjectAccessPolicyView(); |
||||
view.grantedProjectId = this.projectId; |
||||
view.organizationUserId = filtered.id; |
||||
view.read = true; |
||||
view.write = false; |
||||
return view; |
||||
}); |
||||
|
||||
projectAccessPoliciesView.groupAccessPolicies = this.formGroup.value.multiSelect |
||||
?.filter((selection) => selection.icon === this.groupIcon) |
||||
?.map((filtered) => { |
||||
const view = new GroupProjectAccessPolicyView(); |
||||
view.grantedProjectId = this.projectId; |
||||
view.groupId = filtered.id; |
||||
view.read = true; |
||||
view.write = false; |
||||
return view; |
||||
}); |
||||
|
||||
projectAccessPoliciesView.serviceAccountAccessPolicies = this.formGroup.value.multiSelect |
||||
?.filter((selection) => selection.icon === this.serviceAccountIcon) |
||||
?.map((filtered) => { |
||||
const view = new ServiceAccountProjectAccessPolicyView(); |
||||
view.grantedProjectId = this.projectId; |
||||
view.serviceAccountId = filtered.id; |
||||
view.read = true; |
||||
view.write = false; |
||||
return view; |
||||
}); |
||||
return projectAccessPoliciesView; |
||||
} |
||||
|
||||
private getAccessPoliciesCount(projectAccessPoliciesView: ProjectAccessPoliciesView) { |
||||
return ( |
||||
projectAccessPoliciesView.groupAccessPolicies.length + |
||||
projectAccessPoliciesView.serviceAccountAccessPolicies.length + |
||||
projectAccessPoliciesView.userAccessPolicies.length |
||||
); |
||||
} |
||||
|
||||
private filterExistingAccessPolicies( |
||||
potentialGrantees: SelectItemView[], |
||||
projectAccessPolicies: ProjectAccessPoliciesView |
||||
): SelectItemView[] { |
||||
return potentialGrantees |
||||
.filter( |
||||
(potentialGrantee) => |
||||
!projectAccessPolicies.serviceAccountAccessPolicies.some( |
||||
(ap) => ap.serviceAccountId === potentialGrantee.id |
||||
) |
||||
) |
||||
.filter( |
||||
(potentialGrantee) => |
||||
!projectAccessPolicies.userAccessPolicies.some( |
||||
(ap) => ap.organizationUserId === potentialGrantee.id |
||||
) |
||||
) |
||||
.filter( |
||||
(potentialGrantee) => |
||||
!projectAccessPolicies.groupAccessPolicies.some( |
||||
(ap) => ap.groupId === potentialGrantee.id |
||||
) |
||||
); |
||||
} |
||||
|
||||
async updateAccessPolicy(target: any, accessPolicyId: string): Promise<void> { |
||||
try { |
||||
const accessPolicyView = new BaseAccessPolicyView(); |
||||
accessPolicyView.id = accessPolicyId; |
||||
if (target.value === "canRead") { |
||||
accessPolicyView.read = true; |
||||
accessPolicyView.write = false; |
||||
} else if (target.value === "canWrite") { |
||||
accessPolicyView.read = false; |
||||
accessPolicyView.write = true; |
||||
} else if (target.value === "canReadWrite") { |
||||
accessPolicyView.read = true; |
||||
accessPolicyView.write = true; |
||||
} |
||||
|
||||
await this.accessPolicyService.updateAccessPolicy(accessPolicyView); |
||||
} catch (e) { |
||||
this.validationService.showError(e); |
||||
} |
||||
} |
||||
|
||||
delete = (accessPolicyId: string) => async () => { |
||||
this.loading = true; |
||||
this.formGroup.disable(); |
||||
await this.accessPolicyService.deleteAccessPolicy(accessPolicyId); |
||||
return firstValueFrom(this.selectItemsView$); |
||||
}; |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
import { AccessPolicyRequest } from "./access-policy.request"; |
||||
|
||||
export class AccessPoliciesCreateRequest { |
||||
userAccessPolicyRequests?: AccessPolicyRequest[]; |
||||
groupAccessPolicyRequests?: AccessPolicyRequest[]; |
||||
serviceAccountAccessPolicyRequests?: AccessPolicyRequest[]; |
||||
} |
||||
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
export class AccessPolicyUpdateRequest { |
||||
read: boolean; |
||||
write: boolean; |
||||
} |
||||
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
export class AccessPolicyRequest { |
||||
granteeId: string; |
||||
read: boolean; |
||||
write: boolean; |
||||
} |
||||
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response"; |
||||
|
||||
export class BaseAccessPolicyResponse extends BaseResponse { |
||||
id: string; |
||||
read: boolean; |
||||
write: boolean; |
||||
creationDate: string; |
||||
revisionDate: string; |
||||
|
||||
constructor(response: any) { |
||||
super(response); |
||||
this.id = this.getResponseProperty("Id"); |
||||
this.read = this.getResponseProperty("Read"); |
||||
this.write = this.getResponseProperty("Write"); |
||||
this.creationDate = this.getResponseProperty("CreationDate"); |
||||
this.revisionDate = this.getResponseProperty("RevisionDate"); |
||||
} |
||||
} |
||||
|
||||
export class UserProjectAccessPolicyResponse extends BaseAccessPolicyResponse { |
||||
organizationUserId: string; |
||||
organizationUserName: string; |
||||
grantedProjectId: string; |
||||
|
||||
constructor(response: any) { |
||||
super(response); |
||||
this.organizationUserId = this.getResponseProperty("OrganizationUserId"); |
||||
this.organizationUserName = this.getResponseProperty("OrganizationUserName"); |
||||
this.grantedProjectId = this.getResponseProperty("GrantedProjectId"); |
||||
} |
||||
} |
||||
|
||||
export class GroupProjectAccessPolicyResponse extends BaseAccessPolicyResponse { |
||||
groupId: string; |
||||
groupName: string; |
||||
grantedProjectId: string; |
||||
|
||||
constructor(response: any) { |
||||
super(response); |
||||
this.groupId = this.getResponseProperty("GroupId"); |
||||
this.groupName = this.getResponseProperty("GroupName"); |
||||
this.grantedProjectId = this.getResponseProperty("GrantedProjectId"); |
||||
} |
||||
} |
||||
|
||||
export class ServiceAccountProjectAccessPolicyResponse extends BaseAccessPolicyResponse { |
||||
serviceAccountId: string; |
||||
serviceAccountName: string; |
||||
grantedProjectId: string; |
||||
|
||||
constructor(response: any) { |
||||
super(response); |
||||
this.serviceAccountId = this.getResponseProperty("ServiceAccountId"); |
||||
this.serviceAccountName = this.getResponseProperty("ServiceAccountName"); |
||||
this.grantedProjectId = this.getResponseProperty("GrantedProjectId"); |
||||
} |
||||
} |
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response"; |
||||
|
||||
export class PotentialGranteeResponse extends BaseResponse { |
||||
id: string; |
||||
name: string; |
||||
type: string; |
||||
email: string; |
||||
|
||||
constructor(response: any) { |
||||
super(response); |
||||
this.id = this.getResponseProperty("Id"); |
||||
this.name = this.getResponseProperty("Name"); |
||||
this.type = this.getResponseProperty("Type"); |
||||
this.email = this.getResponseProperty("Email"); |
||||
} |
||||
} |
||||
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response"; |
||||
|
||||
import { |
||||
GroupProjectAccessPolicyResponse, |
||||
ServiceAccountProjectAccessPolicyResponse, |
||||
UserProjectAccessPolicyResponse, |
||||
} from "./access-policy.response"; |
||||
|
||||
export class ProjectAccessPoliciesResponse extends BaseResponse { |
||||
userAccessPolicies: UserProjectAccessPolicyResponse[]; |
||||
groupAccessPolicies: GroupProjectAccessPolicyResponse[]; |
||||
serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyResponse[]; |
||||
|
||||
constructor(response: any) { |
||||
super(response); |
||||
this.userAccessPolicies = this.getResponseProperty("UserAccessPolicies"); |
||||
this.groupAccessPolicies = this.getResponseProperty("GroupAccessPolicies"); |
||||
this.serviceAccountAccessPolicies = this.getResponseProperty("ServiceAccountAccessPolicies"); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue