diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index ac97b66acd..6971fbcc9b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5914,7 +5914,7 @@ "message": "Expires" }, "canRead": { - "message": "Can Read" + "message": "Can read" }, "accessTokensNoItemsTitle": { "message": "No access tokens to show" @@ -6141,6 +6141,33 @@ "uploadLicense": { "message": "Upload license" }, + "projectPeopleDescription": { + "message": "Grant groups or people access to this project." + }, + "projectPeopleSelectHint": { + "message": "Type or select people or groups" + }, + "projectServiceAccountsDescription": { + "message": "Grant service accounts access to this project." + }, + "projectServiceAccountsSelectHint": { + "message": "Type or select service accounts" + }, + "projectEmptyPeopleAccessPolicies": { + "message": "Add people or groups to start collaborating" + }, + "projectEmptyServiceAccountAccessPolicies": { + "message": "Add service accounts to grant access" + }, + "canWrite": { + "message": "Can write" + }, + "canReadWrite": { + "message": "Can read, write" + }, + "groupSlashUser": { + "message": "Group/User" + }, "lowKdfIterations": { "message": "Low KDF Iterations" }, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts new file mode 100644 index 0000000000..8af275e63f --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts @@ -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; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/potential-grantee.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/potential-grantee.view.ts new file mode 100644 index 0000000000..a3bf9827d5 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/potential-grantee.view.ts @@ -0,0 +1,6 @@ +export class PotentialGranteeView { + id: string; + name: string; + type: string; + email: string; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/project-access-policies.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/project-access-policies.view.ts new file mode 100644 index 0000000000..5f05b595b6 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/project-access-policies.view.ts @@ -0,0 +1,11 @@ +import { + GroupProjectAccessPolicyView, + ServiceAccountProjectAccessPolicyView, + UserProjectAccessPolicyView, +} from "./access-policy.view"; + +export class ProjectAccessPoliciesView { + userAccessPolicies: UserProjectAccessPolicyView[]; + groupAccessPolicies: GroupProjectAccessPolicyView[]; + serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-access.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-access.component.html new file mode 100644 index 0000000000..4ba5fed402 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-access.component.html @@ -0,0 +1,23 @@ + +
+

+ {{ description }} +

+ + +
+
+ + +
+ +
+
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-access.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-access.component.ts new file mode 100644 index 0000000000..ffe51b8000 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-access.component.ts @@ -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; + protected potentialGrantees$: Observable; + + 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() + ); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html new file mode 100644 index 0000000000..f3a39f4154 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html @@ -0,0 +1,9 @@ + + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts new file mode 100644 index 0000000000..3449272741 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "sm-project-people", + templateUrl: "./project-people.component.html", +}) +export class ProjectPeopleComponent {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html new file mode 100644 index 0000000000..3f82e6d885 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html @@ -0,0 +1,9 @@ + + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts new file mode 100644 index 0000000000..194e5f2d22 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "sm-project-service-accounts", + templateUrl: "./project-service-accounts.component.html", +}) +export class ProjectServiceAccountsComponent {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html index 6044d8d988..f62767c1df 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html @@ -5,7 +5,7 @@ {{ "secrets" | i18n }} {{ "people" | i18n }} - {{ "serviceAccounts" | i18n }} + {{ "serviceAccounts" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts index c0d6642e9d..a5248c509f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts @@ -1,7 +1,9 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { ProjectPeopleComponent } from "./project/project-people.component"; import { ProjectSecretsComponent } from "./project/project-secrets.component"; +import { ProjectServiceAccountsComponent } from "./project/project-service-accounts.component"; import { ProjectComponent } from "./project/project.component"; import { ProjectsComponent } from "./projects/projects.component"; @@ -23,6 +25,14 @@ const routes: Routes = [ path: "secrets", component: ProjectSecretsComponent, }, + { + path: "people", + component: ProjectPeopleComponent, + }, + { + path: "service-accounts", + component: ProjectServiceAccountsComponent, + }, ], }, ]; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects.module.ts index be15bd8306..c6f68db8d8 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects.module.ts @@ -6,7 +6,10 @@ import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; import { ProjectDeleteDialogComponent } from "./dialog/project-delete-dialog.component"; import { ProjectDialogComponent } from "./dialog/project-dialog.component"; +import { ProjectAccessComponent } from "./project/project-access.component"; +import { ProjectPeopleComponent } from "./project/project-people.component"; import { ProjectSecretsComponent } from "./project/project-secrets.component"; +import { ProjectServiceAccountsComponent } from "./project/project-service-accounts.component"; import { ProjectComponent } from "./project/project.component"; import { ProjectsListComponent } from "./projects-list/projects-list.component"; import { ProjectsRoutingModule } from "./projects-routing.module"; @@ -17,8 +20,11 @@ import { ProjectsComponent } from "./projects/projects.component"; declarations: [ ProjectsComponent, ProjectsListComponent, + ProjectAccessComponent, ProjectDialogComponent, ProjectDeleteDialogComponent, + ProjectPeopleComponent, + ProjectServiceAccountsComponent, ProjectComponent, ProjectSecretsComponent, ], diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts new file mode 100644 index 0000000000..a9b7f876c2 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts @@ -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(); + projectAccessPolicies$ = this._projectAccessPolicies.asObservable(); + + constructor( + private cryptoService: CryptoService, + private apiService: ApiService, + private encryptService: EncryptService + ) {} + + async getProjectAccessPolicies( + organizationId: string, + projectId: string + ): Promise { + 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 { + await this.apiService.send("DELETE", "/access-policies/" + accessPolicyId, null, true, false); + this._projectAccessPolicies.next(null); + } + + async updateAccessPolicy(baseAccessPolicyView: BaseAccessPolicyView): Promise { + 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 { + 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 { + 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 { + 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 = this.createBaseAccessPolicyView(response); + view.grantedProjectId = response.grantedProjectId; + view.organizationUserId = response.organizationUserId; + view.organizationUserName = response.organizationUserName; + return view; + } + + private createGroupProjectAccessPolicyView( + response: GroupProjectAccessPolicyResponse + ): GroupProjectAccessPolicyView { + const view = 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 { + const view = 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 { + 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; + }) + ); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.html new file mode 100644 index 0000000000..9af96e797c --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.html @@ -0,0 +1,82 @@ +
+ + {{ label }} + + {{ hint }} + + +
+ + + + + + {{ columnTitle }} + {{ "permissions" | i18n }} + + + + + + + + + + {{ row.name }} + + + + + + + + + + + + + + + + + + + + +
+ {{ emptyMessage }} +
+
+ + +
+ +
+
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.ts new file mode 100644 index 0000000000..885fe67080 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.ts @@ -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; + @Input() potentialGrantees$: Observable; + + private projectId: string; + private organizationId: string; + private destroy$: Subject = new Subject(); + + protected loading = true; + protected formGroup = new FormGroup({ + multiSelect: new FormControl([], [Validators.required]), + }); + + protected selectItemsView$: Observable; + protected rows$: Observable; + + constructor( + private route: ActivatedRoute, + private accessPolicyService: AccessPolicyService, + private validationService: ValidationService + ) {} + + async ngOnInit(): Promise { + 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 { + 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$); + }; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policies-create.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policies-create.request.ts new file mode 100644 index 0000000000..ff391ecacd --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policies-create.request.ts @@ -0,0 +1,7 @@ +import { AccessPolicyRequest } from "./access-policy.request"; + +export class AccessPoliciesCreateRequest { + userAccessPolicyRequests?: AccessPolicyRequest[]; + groupAccessPolicyRequests?: AccessPolicyRequest[]; + serviceAccountAccessPolicyRequests?: AccessPolicyRequest[]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policy-update.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policy-update.request.ts new file mode 100644 index 0000000000..5aff186e12 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policy-update.request.ts @@ -0,0 +1,4 @@ +export class AccessPolicyUpdateRequest { + read: boolean; + write: boolean; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policy.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policy.request.ts new file mode 100644 index 0000000000..527ab3a602 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policy.request.ts @@ -0,0 +1,5 @@ +export class AccessPolicyRequest { + granteeId: string; + read: boolean; + write: boolean; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/access-policy.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/access-policy.response.ts new file mode 100644 index 0000000000..d784dafcdc --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/access-policy.response.ts @@ -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"); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/potential-grantee.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/potential-grantee.response.ts new file mode 100644 index 0000000000..4c8c3eb36e --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/potential-grantee.response.ts @@ -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"); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-access-policies.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-access-policies.response.ts new file mode 100644 index 0000000000..9429dbeb1b --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-access-policies.response.ts @@ -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"); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts index b90021ff56..2dcbb6fecf 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts @@ -1,6 +1,8 @@ import { NgModule } from "@angular/core"; +import { MultiSelectModule } from "@bitwarden/components"; import { ProductSwitcherModule } from "@bitwarden/web-vault/app/layouts/product-switcher/product-switcher.module"; +import { CoreOrganizationModule } from "@bitwarden/web-vault/app/organizations/core"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { BulkStatusDialogComponent } from "../layout/dialogs/bulk-status-dialog.component"; @@ -8,10 +10,11 @@ import { HeaderComponent } from "../layout/header.component"; import { NewMenuComponent } from "../layout/new-menu.component"; import { NoItemsComponent } from "../layout/no-items.component"; +import { AccessSelectorComponent } from "./access-policies/access-selector.component"; import { SecretsListComponent } from "./secrets-list.component"; @NgModule({ - imports: [SharedModule, ProductSwitcherModule], + imports: [SharedModule, ProductSwitcherModule, MultiSelectModule, CoreOrganizationModule], exports: [ SharedModule, BulkStatusDialogComponent, @@ -19,6 +22,7 @@ import { SecretsListComponent } from "./secrets-list.component"; NewMenuComponent, NoItemsComponent, SecretsListComponent, + AccessSelectorComponent, ], declarations: [ BulkStatusDialogComponent, @@ -26,6 +30,7 @@ import { SecretsListComponent } from "./secrets-list.component"; NewMenuComponent, NoItemsComponent, SecretsListComponent, + AccessSelectorComponent, ], providers: [], bootstrap: [],