[SM-910] Migrate service account -> projects tab to new access policy selector (#8572)

* Add view, requests and responses

* access policy service update

* Add read only support to access policy selector

* Migrate service account -> projects tab
This commit is contained in:
Thomas Avery 2024-05-01 11:47:06 -05:00 committed by GitHub
parent a4b3b83c46
commit af0a884ee8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 531 additions and 278 deletions

View File

@ -58,3 +58,12 @@ export class ServiceAccountPeopleAccessPoliciesView {
userAccessPolicies: UserServiceAccountAccessPolicyView[]; userAccessPolicies: UserServiceAccountAccessPolicyView[];
groupAccessPolicies: GroupServiceAccountAccessPolicyView[]; groupAccessPolicies: GroupServiceAccountAccessPolicyView[];
} }
export class ServiceAccountProjectPolicyPermissionDetailsView {
accessPolicy: ServiceAccountProjectAccessPolicyView;
hasPermission: boolean;
}
export class ServiceAccountGrantedPoliciesView {
grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsView[];
}

View File

@ -1,17 +1,27 @@
<div class="tw-mt-4 tw-w-2/5"> <form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner">
<p class="tw-mt-6"> <div class="tw-w-2/5">
{{ "machineAccountProjectsDescription" | i18n }} <p class="tw-mt-8" *ngIf="!loading">
</p> {{ "machineAccountProjectsDescription" | i18n }}
<sm-access-selector </p>
[rows]="rows$ | async" <sm-access-policy-selector
granteeType="projects" [loading]="loading"
[label]="'projects' | i18n" formControlName="accessPolicies"
[hint]="'newSaSelectAccess' | i18n" [addButtonMode]="true"
[columnTitle]="'projects' | i18n" [items]="potentialGrantees"
[emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n" [label]="'projects' | i18n"
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)" [hint]="'newSaSelectAccess' | i18n"
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" [columnTitle]="'projects' | i18n"
(onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)" [emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n"
> >
</sm-access-selector> </sm-access-policy-selector>
</div> <button bitButton buttonType="primary" bitFormButton type="submit" class="tw-mt-7">
{{ "save" | i18n }}
</button>
</div>
</form>
<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>

View File

@ -1,90 +1,68 @@
import { Component, OnDestroy, OnInit } from "@angular/core"; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { combineLatestWith, map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs"; import { combineLatest, Subject, switchMap, takeUntil } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view";
import { ServiceAccountProjectAccessPolicyView } from "../../models/view/access-policy.view"; import { ServiceAccountGrantedPoliciesView } from "../../models/view/access-policy.view";
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
import { import {
AccessSelectorComponent, ApItemValueType,
AccessSelectorRowView, convertToServiceAccountGrantedPoliciesView,
} from "../../shared/access-policies/access-selector.component"; } from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type";
import {
ApItemViewType,
convertPotentialGranteesToApItemViewType,
convertGrantedPoliciesToAccessPolicyItemViews,
} from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type";
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
@Component({ @Component({
selector: "sm-service-account-projects", selector: "sm-service-account-projects",
templateUrl: "./service-account-projects.component.html", templateUrl: "./service-account-projects.component.html",
}) })
export class ServiceAccountProjectsComponent implements OnInit, OnDestroy { export class ServiceAccountProjectsComponent implements OnInit, OnDestroy {
private currentAccessPolicies: ApItemViewType[];
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private serviceAccountId: string;
private organizationId: string; private organizationId: string;
private serviceAccountId: string;
protected rows$: Observable<AccessSelectorRowView[]> = private currentAccessPolicies$ = combineLatest([this.route.params]).pipe(
this.accessPolicyService.serviceAccountGrantedPolicyChanges$.pipe( switchMap(([params]) =>
startWith(null), this.accessPolicyService
combineLatestWith(this.route.params), .getServiceAccountGrantedPolicies(params.organizationId, params.serviceAccountId)
switchMap(([_, params]) => .then((policies) => {
this.accessPolicyService.getGrantedPolicies(params.serviceAccountId, params.organizationId), return convertGrantedPoliciesToAccessPolicyItemViews(policies);
), }),
map((policies) => { ),
return policies.map((policy) => { );
return {
type: "project",
name: policy.grantedProjectName,
id: policy.grantedProjectId,
accessPolicyId: policy.id,
read: policy.read,
write: policy.write,
icon: AccessSelectorComponent.projectIcon,
static: false,
} as AccessSelectorRowView;
});
}),
);
protected handleCreateAccessPolicies(selected: SelectItemView[]) { private potentialGrantees$ = combineLatest([this.route.params]).pipe(
const serviceAccountProjectAccessPolicyView = selected switchMap(([params]) =>
.filter((selection) => AccessSelectorComponent.getAccessItemType(selection) === "project") this.accessPolicyService
.map((filtered) => { .getProjectsPotentialGrantees(params.organizationId)
const view = new ServiceAccountProjectAccessPolicyView(); .then((grantees) => {
view.serviceAccountId = this.serviceAccountId; return convertPotentialGranteesToApItemViewType(grantees);
view.grantedProjectId = filtered.id; }),
view.read = true; ),
view.write = false; );
return view;
});
return this.accessPolicyService.createGrantedPolicies( protected formGroup = new FormGroup({
this.organizationId, accessPolicies: new FormControl([] as ApItemValueType[]),
this.serviceAccountId, });
serviceAccountProjectAccessPolicyView,
);
}
protected async handleUpdateAccessPolicy(policy: AccessSelectorRowView) { protected loading = true;
try { protected potentialGrantees: ApItemViewType[];
return await this.accessPolicyService.updateAccessPolicy(
AccessSelectorComponent.getBaseAccessPolicyView(policy),
);
} catch (e) {
this.validationService.showError(e);
}
}
protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) {
try {
await this.accessPolicyService.deleteAccessPolicy(policy.accessPolicyId);
} catch (e) {
this.validationService.showError(e);
}
}
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private changeDetectorRef: ChangeDetectorRef,
private validationService: ValidationService, private validationService: ValidationService,
private accessPolicyService: AccessPolicyService, private accessPolicyService: AccessPolicyService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@ -92,10 +70,119 @@ export class ServiceAccountProjectsComponent implements OnInit, OnDestroy {
this.organizationId = params.organizationId; this.organizationId = params.organizationId;
this.serviceAccountId = params.serviceAccountId; this.serviceAccountId = params.serviceAccountId;
}); });
combineLatest([this.potentialGrantees$, this.currentAccessPolicies$])
.pipe(takeUntil(this.destroy$))
.subscribe(([potentialGrantees, currentAccessPolicies]) => {
this.potentialGrantees = this.getPotentialGrantees(
potentialGrantees,
currentAccessPolicies,
);
this.setSelected(currentAccessPolicies);
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
} }
submit = async () => {
if (this.isFormInvalid()) {
return;
}
const formValues = this.getFormValues();
this.formGroup.disable();
try {
const grantedViews = await this.updateServiceAccountGrantedPolicies(
this.organizationId,
this.serviceAccountId,
formValues,
);
this.currentAccessPolicies = convertGrantedPoliciesToAccessPolicyItemViews(grantedViews);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("serviceAccountAccessUpdated"),
);
} catch (e) {
this.validationService.showError(e);
this.setSelected(this.currentAccessPolicies);
}
this.formGroup.enable();
};
private setSelected(policiesToSelect: ApItemViewType[]) {
this.loading = true;
this.currentAccessPolicies = policiesToSelect;
if (policiesToSelect != undefined) {
// Must detect changes so that AccessSelector @Inputs() are aware of the latest
// potentialGrantees, otherwise no selected values will be patched below
this.changeDetectorRef.detectChanges();
this.formGroup.patchValue({
accessPolicies: policiesToSelect.map((m) => ({
type: m.type,
id: m.id,
permission: m.permission,
readOnly: m.readOnly,
})),
});
}
this.loading = false;
}
private isFormInvalid(): boolean {
this.formGroup.markAllAsTouched();
return this.formGroup.invalid;
}
private async updateServiceAccountGrantedPolicies(
organizationId: string,
serviceAccountId: string,
selectedPolicies: ApItemValueType[],
): Promise<ServiceAccountGrantedPoliciesView> {
const grantedViews = convertToServiceAccountGrantedPoliciesView(
serviceAccountId,
selectedPolicies,
);
return await this.accessPolicyService.putServiceAccountGrantedPolicies(
organizationId,
serviceAccountId,
grantedViews,
);
}
private getPotentialGrantees(
potentialGrantees: ApItemViewType[],
currentAccessPolicies: ApItemViewType[],
) {
// If the user doesn't have access to the project, they won't be in the potentialGrantees list.
// Add them to the potentialGrantees list so they can be selected as read-only.
for (const policy of currentAccessPolicies) {
const exists = potentialGrantees.some((grantee) => grantee.id === policy.id);
if (!exists) {
potentialGrantees.push(policy);
}
}
return potentialGrantees;
}
private getFormValues(): ApItemValueType[] {
// The read-only disabled form values are not included in the formGroup value.
// Manually add them to the returned result to ensure they are included in the form submission.
let formValues = this.formGroup.value.accessPolicies;
formValues = formValues.concat(
this.currentAccessPolicies
.filter((m) => m.readOnly)
.map((m) => ({
id: m.id,
type: m.type,
permission: m.permission,
})),
);
return formValues;
}
} }

View File

@ -29,14 +29,17 @@
bitRow bitRow
*ngFor="let item of selectionList.selectedItems; let i = index" *ngFor="let item of selectionList.selectedItems; let i = index"
[formGroupName]="i" [formGroupName]="i"
[ngClass]="{ 'tw-text-muted': item.readOnly }"
> >
<td bitCell class="tw-w-0 tw-pr-0"> <td bitCell class="tw-w-0 tw-pr-0">
<i class="bwi {{ item.icon }} tw-text-muted" aria-hidden="true"></i> <i class="bwi {{ item.icon }}" aria-hidden="true"></i>
</td>
<td bitCell class="tw-max-w-sm tw-truncate">
{{ item.labelName }}
</td> </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"> <td bitCell class="tw-mb-auto tw-inline-block tw-w-auto">
<select <select
*ngIf="!staticPermission; else static" *ngIf="!staticPermission && !item.readOnly; else readOnly"
bitInput bitInput
formControlName="permission" formControlName="permission"
(blur)="handleBlur()" (blur)="handleBlur()"
@ -45,12 +48,20 @@
{{ p.labelId | i18n }} {{ p.labelId | i18n }}
</option> </option>
</select> </select>
<ng-template #readOnly>
<ng-container *ngIf="item.readOnly; else static">
<div class="tw-overflow-hidden tw-overflow-ellipsis tw-whitespace-nowrap">
{{ item.permission | i18n }}
</div>
</ng-container>
</ng-template>
<ng-template #static> <ng-template #static>
<span>{{ staticPermission | i18n }}</span> <span>{{ staticPermission | i18n }}</span>
</ng-template> </ng-template>
</td> </td>
<td bitCell class="tw-w-0"> <td bitCell class="tw-w-0">
<button <button
*ngIf="!item.readOnly"
type="button" type="button"
bitIconButton="bwi-close" bitIconButton="bwi-close"
buttonType="main" buttonType="main"

View File

@ -35,6 +35,34 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn
private notifyOnTouch: () => void; private notifyOnTouch: () => void;
private pauseChangeNotification: boolean; private pauseChangeNotification: boolean;
/**
* Updates the enabled/disabled state of provided row form group based on the item's readonly state.
* If a row is enabled, it also updates the enabled/disabled state of the permission control
* based on the item's accessAllItems state and the current value of `permissionMode`.
* @param controlRow - The form group for the row to update
* @param item - The access item that is represented by the row
*/
private updateRowControlDisableState = (
controlRow: FormGroup<ControlsOf<ApItemValueType>>,
item: ApItemViewType,
) => {
// Disable entire row form group if readOnly
if (item.readOnly || this.disabled) {
controlRow.disable();
} else {
controlRow.enable();
}
};
/**
* Updates the enabled/disabled state of ALL row form groups based on each item's readonly state.
*/
private updateAllRowControlDisableStates = () => {
this.selectionList.forEachControlItem((controlRow, item) => {
this.updateRowControlDisableState(controlRow as FormGroup<ControlsOf<ApItemValueType>>, item);
});
};
/** /**
* The internal selection list that tracks the value of this form control / component. * 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 * It's responsible for keeping items sorted and synced with the rendered form controls
@ -59,6 +87,9 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn
currentUserInGroup: new FormControl(currentUserInGroup), currentUserInGroup: new FormControl(currentUserInGroup),
currentUser: new FormControl(currentUser), currentUser: new FormControl(currentUser),
}); });
this.updateRowControlDisableState(fg, item);
return fg; return fg;
}, this._itemComparator.bind(this)); }, this._itemComparator.bind(this));
@ -100,7 +131,13 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn
set items(val: ApItemViewType[]) { set items(val: ApItemViewType[]) {
if (val != null) { if (val != null) {
const selected = this.selectionList.formArray.getRawValue() ?? []; let selected = this.selectionList.formArray.getRawValue() ?? [];
selected = selected.concat(
val
.filter((m) => m.readOnly)
.map((m) => ({ id: m.id, type: m.type, permission: m.permission })),
);
this.selectionList.populateItems( this.selectionList.populateItems(
val.map((m) => { val.map((m) => {
m.icon = m.icon ?? ApItemEnumUtil.itemIcon(m.type); m.icon = m.icon ?? ApItemEnumUtil.itemIcon(m.type);
@ -137,6 +174,9 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn
} else { } else {
this.formGroup.enable(); this.formGroup.enable();
this.multiSelectFormGroup.enable(); this.multiSelectFormGroup.enable();
// The enable() above automatically enables all the row controls,
// so we need to disable the readonly ones again
this.updateAllRowControlDisableStates();
} }
} }
@ -149,6 +189,9 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn
// Always clear the internal selection list on a new value // Always clear the internal selection list on a new value
this.selectionList.deselectAll(); this.selectionList.deselectAll();
// We need to also select any read only items to appear in the table
this.selectionList.selectItems(this.items.filter((m) => m.readOnly).map((m) => m.id));
// If the new value is null, then we're done // If the new value is null, then we're done
if (selectedItems == null) { if (selectedItems == null) {
this.pauseChangeNotification = false; this.pauseChangeNotification = false;

View File

@ -347,6 +347,7 @@ function createApItemViewType(options: Partial<ApItemViewType> = {}) {
labelName: options?.labelName ?? "test", labelName: options?.labelName ?? "test",
type: options?.type ?? ApItemEnum.User, type: options?.type ?? ApItemEnum.User,
permission: options?.permission ?? ApPermissionEnum.CanRead, permission: options?.permission ?? ApPermissionEnum.CanRead,
readOnly: options?.readOnly ?? false,
}; };
} }

View File

@ -5,6 +5,9 @@ import {
ServiceAccountPeopleAccessPoliciesView, ServiceAccountPeopleAccessPoliciesView,
UserServiceAccountAccessPolicyView, UserServiceAccountAccessPolicyView,
GroupServiceAccountAccessPolicyView, GroupServiceAccountAccessPolicyView,
ServiceAccountGrantedPoliciesView,
ServiceAccountProjectPolicyPermissionDetailsView,
ServiceAccountProjectAccessPolicyView,
} from "../../../../models/view/access-policy.view"; } from "../../../../models/view/access-policy.view";
import { ApItemEnum } from "./enums/ap-item.enum"; import { ApItemEnum } from "./enums/ap-item.enum";
@ -76,3 +79,26 @@ export function convertToServiceAccountPeopleAccessPoliciesView(
}); });
return view; return view;
} }
export function convertToServiceAccountGrantedPoliciesView(
serviceAccountId: string,
selectedPolicyValues: ApItemValueType[],
): ServiceAccountGrantedPoliciesView {
const view = new ServiceAccountGrantedPoliciesView();
view.grantedProjectPolicies = selectedPolicyValues
.filter((x) => x.type == ApItemEnum.Project)
.map((filtered) => {
const detailView = new ServiceAccountProjectPolicyPermissionDetailsView();
const policyView = new ServiceAccountProjectAccessPolicyView();
policyView.serviceAccountId = serviceAccountId;
policyView.grantedProjectId = filtered.id;
policyView.read = ApPermissionEnumUtil.toRead(filtered.permission);
policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission);
detailView.accessPolicy = policyView;
return detailView;
});
return view;
}

View File

@ -3,6 +3,7 @@ import { SelectItemView } from "@bitwarden/components";
import { import {
ProjectPeopleAccessPoliciesView, ProjectPeopleAccessPoliciesView,
ServiceAccountGrantedPoliciesView,
ServiceAccountPeopleAccessPoliciesView, ServiceAccountPeopleAccessPoliciesView,
} from "../../../../models/view/access-policy.view"; } from "../../../../models/view/access-policy.view";
import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view"; import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view";
@ -13,6 +14,12 @@ import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.en
export type ApItemViewType = SelectItemView & { export type ApItemViewType = SelectItemView & {
accessPolicyId?: string; accessPolicyId?: string;
permission?: ApPermissionEnum; permission?: ApPermissionEnum;
/**
* Flag that this item cannot be modified.
* This will disable the permission editor and will keep
* the item always selected.
*/
readOnly: boolean;
} & ( } & (
| { | {
type: ApItemEnum.User; type: ApItemEnum.User;
@ -47,6 +54,7 @@ export function convertToAccessPolicyItemViews(
permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write), permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write),
userId: policy.userId, userId: policy.userId,
currentUser: policy.currentUser, currentUser: policy.currentUser,
readOnly: false,
}); });
}); });
@ -60,12 +68,36 @@ export function convertToAccessPolicyItemViews(
listName: policy.groupName, listName: policy.groupName,
permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write), permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write),
currentUserInGroup: policy.currentUserInGroup, currentUserInGroup: policy.currentUserInGroup,
readOnly: false,
}); });
}); });
return accessPolicies; return accessPolicies;
} }
export function convertGrantedPoliciesToAccessPolicyItemViews(
value: ServiceAccountGrantedPoliciesView,
): ApItemViewType[] {
const accessPolicies: ApItemViewType[] = [];
value.grantedProjectPolicies.forEach((detailView) => {
accessPolicies.push({
type: ApItemEnum.Project,
icon: ApItemEnumUtil.itemIcon(ApItemEnum.Project),
id: detailView.accessPolicy.grantedProjectId,
accessPolicyId: detailView.accessPolicy.id,
labelName: detailView.accessPolicy.grantedProjectName,
listName: detailView.accessPolicy.grantedProjectName,
permission: ApPermissionEnumUtil.toApPermissionEnum(
detailView.accessPolicy.read,
detailView.accessPolicy.write,
),
readOnly: !detailView.hasPermission,
});
});
return accessPolicies;
}
export function convertPotentialGranteesToApItemViewType( export function convertPotentialGranteesToApItemViewType(
grantees: PotentialGranteeView[], grantees: PotentialGranteeView[],
): ApItemViewType[] { ): ApItemViewType[] {
@ -108,6 +140,7 @@ export function convertPotentialGranteesToApItemViewType(
listName: listName, listName: listName,
currentUserInGroup: granteeView.currentUserInGroup, currentUserInGroup: granteeView.currentUserInGroup,
currentUser: granteeView.currentUser, currentUser: granteeView.currentUser,
readOnly: false,
}; };
}); });
} }

View File

@ -18,15 +18,17 @@ import {
UserProjectAccessPolicyView, UserProjectAccessPolicyView,
UserServiceAccountAccessPolicyView, UserServiceAccountAccessPolicyView,
ServiceAccountPeopleAccessPoliciesView, ServiceAccountPeopleAccessPoliciesView,
ServiceAccountGrantedPoliciesView,
ServiceAccountProjectPolicyPermissionDetailsView,
} from "../../models/view/access-policy.view"; } from "../../models/view/access-policy.view";
import { PotentialGranteeView } from "../../models/view/potential-grantee.view"; import { PotentialGranteeView } from "../../models/view/potential-grantee.view";
import { AccessPoliciesCreateRequest } from "../../shared/access-policies/models/requests/access-policies-create.request"; import { AccessPoliciesCreateRequest } from "../../shared/access-policies/models/requests/access-policies-create.request";
import { PeopleAccessPoliciesRequest } from "../../shared/access-policies/models/requests/people-access-policies.request"; import { PeopleAccessPoliciesRequest } from "../../shared/access-policies/models/requests/people-access-policies.request";
import { ProjectAccessPoliciesResponse } from "../../shared/access-policies/models/responses/project-access-policies.response"; import { ProjectAccessPoliciesResponse } from "../../shared/access-policies/models/responses/project-access-policies.response";
import { ServiceAccountGrantedPoliciesRequest } from "../access-policies/models/requests/service-account-granted-policies.request";
import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request"; import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request";
import { AccessPolicyRequest } from "./models/requests/access-policy.request"; import { AccessPolicyRequest } from "./models/requests/access-policy.request";
import { GrantedPolicyRequest } from "./models/requests/granted-policy.request";
import { import {
GroupServiceAccountAccessPolicyResponse, GroupServiceAccountAccessPolicyResponse,
UserServiceAccountAccessPolicyResponse, UserServiceAccountAccessPolicyResponse,
@ -36,28 +38,21 @@ import {
} from "./models/responses/access-policy.response"; } from "./models/responses/access-policy.response";
import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response"; import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response";
import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response"; import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response";
import { ServiceAccountGrantedPoliciesPermissionDetailsResponse } from "./models/responses/service-account-granted-policies-permission-details.response";
import { ServiceAccountPeopleAccessPoliciesResponse } from "./models/responses/service-account-people-access-policies.response"; import { ServiceAccountPeopleAccessPoliciesResponse } from "./models/responses/service-account-people-access-policies.response";
import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./models/responses/service-account-project-policy-permission-details.response";
@Injectable({ @Injectable({
providedIn: "root", providedIn: "root",
}) })
export class AccessPolicyService { export class AccessPolicyService {
private _projectAccessPolicyChanges$ = new Subject<ProjectAccessPoliciesView>(); private _projectAccessPolicyChanges$ = new Subject<ProjectAccessPoliciesView>();
private _serviceAccountGrantedPolicyChanges$ = new Subject<
ServiceAccountProjectAccessPolicyView[]
>();
/** /**
* Emits when a project access policy is created or deleted. * Emits when a project access policy is created or deleted.
*/ */
readonly projectAccessPolicyChanges$ = this._projectAccessPolicyChanges$.asObservable(); readonly projectAccessPolicyChanges$ = this._projectAccessPolicyChanges$.asObservable();
/**
* Emits when a service account granted policy is created or deleted.
*/
readonly serviceAccountGrantedPolicyChanges$ =
this._serviceAccountGrantedPolicyChanges$.asObservable();
constructor( constructor(
private cryptoService: CryptoService, private cryptoService: CryptoService,
protected apiService: ApiService, protected apiService: ApiService,
@ -68,44 +63,6 @@ export class AccessPolicyService {
this._projectAccessPolicyChanges$.next(null); this._projectAccessPolicyChanges$.next(null);
} }
async getGrantedPolicies(
serviceAccountId: string,
organizationId: string,
): Promise<ServiceAccountProjectAccessPolicyView[]> {
const r = await this.apiService.send(
"GET",
"/service-accounts/" + serviceAccountId + "/granted-policies",
null,
true,
true,
);
const results = new ListResponse(r, ServiceAccountProjectAccessPolicyResponse);
return await this.createServiceAccountProjectAccessPolicyViews(results.data, organizationId);
}
async createGrantedPolicies(
organizationId: string,
serviceAccountId: string,
policies: ServiceAccountProjectAccessPolicyView[],
): Promise<ServiceAccountProjectAccessPolicyView[]> {
const request = this.getGrantedPoliciesCreateRequest(policies);
const r = await this.apiService.send(
"POST",
"/service-accounts/" + serviceAccountId + "/granted-policies",
request,
true,
true,
);
const results = new ListResponse(r, ServiceAccountProjectAccessPolicyResponse);
const views = await this.createServiceAccountProjectAccessPolicyViews(
results.data,
organizationId,
);
this._serviceAccountGrantedPolicyChanges$.next(views);
return views;
}
async getProjectAccessPolicies( async getProjectAccessPolicies(
organizationId: string, organizationId: string,
projectId: string, projectId: string,
@ -184,6 +141,40 @@ export class AccessPolicyService {
return this.createServiceAccountPeopleAccessPoliciesView(results); return this.createServiceAccountPeopleAccessPoliciesView(results);
} }
async getServiceAccountGrantedPolicies(
organizationId: string,
serviceAccountId: string,
): Promise<ServiceAccountGrantedPoliciesView> {
const r = await this.apiService.send(
"GET",
"/service-accounts/" + serviceAccountId + "/granted-policies",
null,
true,
true,
);
const result = new ServiceAccountGrantedPoliciesPermissionDetailsResponse(r);
return await this.createServiceAccountGrantedPoliciesView(result, organizationId);
}
async putServiceAccountGrantedPolicies(
organizationId: string,
serviceAccountId: string,
policies: ServiceAccountGrantedPoliciesView,
): Promise<ServiceAccountGrantedPoliciesView> {
const request = this.getServiceAccountGrantedPoliciesRequest(policies);
const r = await this.apiService.send(
"PUT",
"/service-accounts/" + serviceAccountId + "/granted-policies",
request,
true,
true,
);
const result = new ServiceAccountGrantedPoliciesPermissionDetailsResponse(r);
return await this.createServiceAccountGrantedPoliciesView(result, organizationId);
}
async createProjectAccessPolicies( async createProjectAccessPolicies(
organizationId: string, organizationId: string,
projectId: string, projectId: string,
@ -206,7 +197,6 @@ export class AccessPolicyService {
async deleteAccessPolicy(accessPolicyId: string): Promise<void> { async deleteAccessPolicy(accessPolicyId: string): Promise<void> {
await this.apiService.send("DELETE", "/access-policies/" + accessPolicyId, null, true, false); await this.apiService.send("DELETE", "/access-policies/" + accessPolicyId, null, true, false);
this._projectAccessPolicyChanges$.next(null); this._projectAccessPolicyChanges$.next(null);
this._serviceAccountGrantedPolicyChanges$.next(null);
} }
async updateAccessPolicy(baseAccessPolicyView: BaseAccessPolicyView): Promise<void> { async updateAccessPolicy(baseAccessPolicyView: BaseAccessPolicyView): Promise<void> {
@ -222,6 +212,158 @@ export class AccessPolicyService {
); );
} }
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 getProjectsPotentialGrantees(organizationId: string) {
const r = await this.apiService.send(
"GET",
"/organizations/" + organizationId + "/access-policies/projects/potential-grantees",
null,
true,
true,
);
const results = new ListResponse(r, PotentialGranteeResponse);
return await this.createPotentialGranteeViews(organizationId, results.data);
}
protected async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
return await this.cryptoService.getOrgKey(organizationId);
}
protected getAccessPolicyRequest(
granteeId: string,
view:
| UserProjectAccessPolicyView
| UserServiceAccountAccessPolicyView
| GroupProjectAccessPolicyView
| GroupServiceAccountAccessPolicyView
| ServiceAccountProjectAccessPolicyView,
) {
const request = new AccessPolicyRequest();
request.granteeId = granteeId;
request.read = view.read;
request.write = view.write;
return request;
}
protected createBaseAccessPolicyView(
response:
| UserProjectAccessPolicyResponse
| UserServiceAccountAccessPolicyResponse
| GroupProjectAccessPolicyResponse
| GroupServiceAccountAccessPolicyResponse
| 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;
view.currentUser = r.currentUser;
view.currentUserInGroup = r.currentUserInGroup;
if (r.type === "serviceAccount" || r.type === "project") {
view.name = r.name
? await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey)
: null;
} else {
view.name = r.name;
}
return view;
}),
);
}
private getServiceAccountGrantedPoliciesRequest(
policies: ServiceAccountGrantedPoliciesView,
): ServiceAccountGrantedPoliciesRequest {
const request = new ServiceAccountGrantedPoliciesRequest();
request.projectGrantedPolicyRequests = policies.grantedProjectPolicies.map((detailView) => ({
grantedId: detailView.accessPolicy.grantedProjectId,
read: detailView.accessPolicy.read,
write: detailView.accessPolicy.write,
}));
return request;
}
private async createServiceAccountGrantedPoliciesView(
response: ServiceAccountGrantedPoliciesPermissionDetailsResponse,
organizationId: string,
): Promise<ServiceAccountGrantedPoliciesView> {
const orgKey = await this.getOrganizationKey(organizationId);
const view = new ServiceAccountGrantedPoliciesView();
view.grantedProjectPolicies =
await this.createServiceAccountProjectPolicyPermissionDetailsViews(
orgKey,
response.grantedProjectPolicies,
);
return view;
}
private async createServiceAccountProjectPolicyPermissionDetailsViews(
orgKey: SymmetricCryptoKey,
responses: ServiceAccountProjectPolicyPermissionDetailsResponse[],
): Promise<ServiceAccountProjectPolicyPermissionDetailsView[]> {
return await Promise.all(
responses.map(async (response) => {
return await this.createServiceAccountProjectPolicyPermissionDetailsView(orgKey, response);
}),
);
}
private async createServiceAccountProjectPolicyPermissionDetailsView(
orgKey: SymmetricCryptoKey,
response: ServiceAccountProjectPolicyPermissionDetailsResponse,
): Promise<ServiceAccountProjectPolicyPermissionDetailsView> {
const view = new ServiceAccountProjectPolicyPermissionDetailsView();
view.hasPermission = response.hasPermission;
view.accessPolicy = await this.createServiceAccountProjectAccessPolicyView(
orgKey,
response.accessPolicy,
);
return view;
}
private async createProjectAccessPoliciesView( private async createProjectAccessPoliciesView(
organizationId: string, organizationId: string,
projectAccessPoliciesResponse: ProjectAccessPoliciesResponse, projectAccessPoliciesResponse: ProjectAccessPoliciesResponse,
@ -393,147 +535,4 @@ export class AccessPolicyService {
currentUserInGroup: response.currentUserInGroup, currentUserInGroup: response.currentUserInGroup,
}; };
} }
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 getProjectsPotentialGrantees(organizationId: string) {
const r = await this.apiService.send(
"GET",
"/organizations/" + organizationId + "/access-policies/projects/potential-grantees",
null,
true,
true,
);
const results = new ListResponse(r, PotentialGranteeResponse);
return await this.createPotentialGranteeViews(organizationId, results.data);
}
protected async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
return await this.cryptoService.getOrgKey(organizationId);
}
protected getAccessPolicyRequest(
granteeId: string,
view:
| UserProjectAccessPolicyView
| UserServiceAccountAccessPolicyView
| GroupProjectAccessPolicyView
| GroupServiceAccountAccessPolicyView
| ServiceAccountProjectAccessPolicyView,
) {
const request = new AccessPolicyRequest();
request.granteeId = granteeId;
request.read = view.read;
request.write = view.write;
return request;
}
protected createBaseAccessPolicyView(
response:
| UserProjectAccessPolicyResponse
| UserServiceAccountAccessPolicyResponse
| GroupProjectAccessPolicyResponse
| GroupServiceAccountAccessPolicyResponse
| 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;
view.currentUser = r.currentUser;
view.currentUserInGroup = r.currentUserInGroup;
if (r.type === "serviceAccount" || r.type === "project") {
view.name = r.name
? await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey)
: null;
} else {
view.name = r.name;
}
return view;
}),
);
}
private getGrantedPoliciesCreateRequest(
policies: ServiceAccountProjectAccessPolicyView[],
): GrantedPolicyRequest[] {
return policies.map((ap) => {
const request = new GrantedPolicyRequest();
request.grantedId = ap.grantedProjectId;
request.read = ap.read;
request.write = ap.write;
return request;
});
}
private async createServiceAccountProjectAccessPolicyViews(
responses: ServiceAccountProjectAccessPolicyResponse[],
organizationId: string,
): Promise<ServiceAccountProjectAccessPolicyView[]> {
const orgKey = await this.getOrganizationKey(organizationId);
return await Promise.all(
responses.map(async (response: ServiceAccountProjectAccessPolicyResponse) => {
const view = new ServiceAccountProjectAccessPolicyView();
view.id = response.id;
view.read = response.read;
view.write = response.write;
view.creationDate = response.creationDate;
view.revisionDate = response.revisionDate;
view.serviceAccountId = response.serviceAccountId;
view.grantedProjectId = response.grantedProjectId;
view.serviceAccountName = response.serviceAccountName
? await this.encryptService.decryptToUtf8(
new EncString(response.serviceAccountName),
orgKey,
)
: null;
view.grantedProjectName = response.grantedProjectName
? await this.encryptService.decryptToUtf8(
new EncString(response.grantedProjectName),
orgKey,
)
: null;
return view;
}),
);
}
} }

View File

@ -0,0 +1,5 @@
import { GrantedPolicyRequest } from "./granted-policy.request";
export class ServiceAccountGrantedPoliciesRequest {
projectGrantedPolicyRequests?: GrantedPolicyRequest[];
}

View File

@ -0,0 +1,15 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./service-account-project-policy-permission-details.response";
export class ServiceAccountGrantedPoliciesPermissionDetailsResponse extends BaseResponse {
grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsResponse[];
constructor(response: any) {
super(response);
const grantedProjectPolicies = this.getResponseProperty("GrantedProjectPolicies");
this.grantedProjectPolicies = grantedProjectPolicies.map(
(k: any) => new ServiceAccountProjectPolicyPermissionDetailsResponse(k),
);
}
}

View File

@ -0,0 +1,14 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { ServiceAccountProjectAccessPolicyResponse } from "./access-policy.response";
export class ServiceAccountProjectPolicyPermissionDetailsResponse extends BaseResponse {
accessPolicy: ServiceAccountProjectAccessPolicyResponse;
hasPermission: boolean;
constructor(response: any) {
super(response);
this.accessPolicy = this.getResponseProperty("AccessPolicy");
this.hasPermission = this.getResponseProperty("HasPermission");
}
}