diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 2773efc17f..786ebea9f4 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6570,6 +6570,27 @@ "secretsManagerEnable": { "message": "Enable Secrets Manager Beta" }, + "saPeopleWarningTitle": { + "message": "Access tokens still available" + }, + "saPeopleWarningMessage": { + "message": "Removing people from a service account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a service account." + }, + "smAccessRemovalWarningProjectTitle": { + "message": "Remove access to this project" + }, + "smAccessRemovalWarningProjectMessage": { + "message": "This action will remove your access to the project." + }, + "smAccessRemovalWarningSaTitle": { + "message": "Remove access to this service account" + }, + "smAccessRemovalWarningSaMessage": { + "message": "This action will remove your access to the service account." + }, + "removeAccess": { + "message": "Remove access" + }, "checkForBreaches": { "message": "Check known data breaches for this password" }, 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 index 0f93b549f3..ac6edfe39e 100644 --- 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 @@ -10,24 +10,28 @@ export class UserProjectAccessPolicyView extends BaseAccessPolicyView { organizationUserId: string; organizationUserName: string; grantedProjectId: string; + userId: string; } export class UserServiceAccountAccessPolicyView extends BaseAccessPolicyView { organizationUserId: string; organizationUserName: string; grantedServiceAccountId: string; + userId: string; } export class GroupProjectAccessPolicyView extends BaseAccessPolicyView { groupId: string; groupName: string; grantedProjectId: string; + currentUserInGroup: boolean; } export class GroupServiceAccountAccessPolicyView extends BaseAccessPolicyView { groupId: string; groupName: string; grantedServiceAccountId: string; + currentUserInGroup: boolean; } export class ServiceAccountProjectAccessPolicyView extends BaseAccessPolicyView { 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 index 124eafb8de..b461c10a3b 100644 --- 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 @@ -10,6 +10,8 @@ [columnTitle]="'groupSlashUser' | i18n" [emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n" (onCreateAccessPolicies)="handleCreateAccessPolicies($event)" + (onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" + (onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)" > 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 index 4a1ea9c7fd..5dda1a5a54 100644 --- 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 @@ -1,8 +1,9 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs"; +import { map, Observable, share, startWith, Subject, switchMap, takeUntil } from "rxjs"; -import { SelectItemView } from "@bitwarden/components"; +import { ValidationService } from "@bitwarden/common/abstractions/validation.service"; +import { DialogService, SelectItemView } from "@bitwarden/components"; import { GroupProjectAccessPolicyView, @@ -14,6 +15,10 @@ import { AccessSelectorComponent, AccessSelectorRowView, } from "../../shared/access-policies/access-selector.component"; +import { + AccessRemovalDetails, + AccessRemovalDialogComponent, +} from "../../shared/access-policies/dialogs/access-removal-dialog.component"; @Component({ selector: "sm-project-people", @@ -23,6 +28,7 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); private organizationId: string; private projectId: string; + private rows: AccessSelectorRowView[]; protected rows$: Observable = this.accessPolicyService.projectAccessPolicyChanges$.pipe( @@ -40,6 +46,7 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy { accessPolicyId: policy.id, read: policy.read, write: policy.write, + userId: policy.userId, icon: AccessSelectorComponent.userIcon, }); }); @@ -52,11 +59,13 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy { accessPolicyId: policy.id, read: policy.read, write: policy.write, + currentUserInGroup: policy.currentUserInGroup, icon: AccessSelectorComponent.groupIcon, }); }); return rows; - }) + }), + share() ); protected handleCreateAccessPolicies(selected: SelectItemView[]) { @@ -90,17 +99,94 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy { ); } - constructor(private route: ActivatedRoute, private accessPolicyService: AccessPolicyService) {} + protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) { + if ( + await this.accessPolicyService.needToShowAccessRemovalWarning( + this.organizationId, + policy, + this.rows + ) + ) { + this.launchDeleteWarningDialog(policy); + return; + } + + try { + await this.accessPolicyService.deleteAccessPolicy(policy.accessPolicyId); + } catch (e) { + this.validationService.showError(e); + } + } + + protected async handleUpdateAccessPolicy(policy: AccessSelectorRowView) { + if ( + policy.read === true && + policy.write === false && + (await this.accessPolicyService.needToShowAccessRemovalWarning( + this.organizationId, + policy, + this.rows + )) + ) { + this.launchUpdateWarningDialog(policy); + return; + } + + try { + return await this.accessPolicyService.updateAccessPolicy( + AccessSelectorComponent.getBaseAccessPolicyView(policy) + ); + } catch (e) { + this.validationService.showError(e); + } + } + + constructor( + private route: ActivatedRoute, + private dialogService: DialogService, + private validationService: ValidationService, + private accessPolicyService: AccessPolicyService + ) {} ngOnInit(): void { this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { this.organizationId = params.organizationId; this.projectId = params.projectId; }); + + this.rows$.pipe(takeUntil(this.destroy$)).subscribe((rows) => { + this.rows = rows; + }); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } + + private async launchDeleteWarningDialog(policy: AccessSelectorRowView) { + this.dialogService.open(AccessRemovalDialogComponent, { + data: { + title: "smAccessRemovalWarningProjectTitle", + message: "smAccessRemovalWarningProjectMessage", + operation: "delete", + type: "project", + returnRoute: ["sm", this.organizationId, "projects"], + policy, + }, + }); + } + + private launchUpdateWarningDialog(policy: AccessSelectorRowView) { + this.dialogService.open(AccessRemovalDialogComponent, { + data: { + title: "smAccessRemovalWarningProjectTitle", + message: "smAccessRemovalWarningProjectMessage", + operation: "update", + type: "project", + returnRoute: ["sm", this.organizationId, "projects"], + policy, + }, + }); + } } 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 index 12bdd05316..fb6eab471b 100644 --- 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 @@ -10,6 +10,7 @@ [columnTitle]="'serviceAccounts' | i18n" [emptyMessage]="'projectEmptyServiceAccountAccessPolicies' | i18n" (onCreateAccessPolicies)="handleCreateAccessPolicies($event)" + (onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" > 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 index e46c3038b9..19105c6cf8 100644 --- 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 @@ -2,6 +2,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs"; +import { ValidationService } from "@bitwarden/common/abstractions/validation.service"; import { SelectItemView } from "@bitwarden/components"; import { @@ -65,7 +66,19 @@ export class ProjectServiceAccountsComponent implements OnInit, OnDestroy { ); } - constructor(private route: ActivatedRoute, private accessPolicyService: AccessPolicyService) {} + protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) { + try { + await this.accessPolicyService.deleteAccessPolicy(policy.accessPolicyId); + } catch (e) { + this.validationService.showError(e); + } + } + + constructor( + private route: ActivatedRoute, + private validationService: ValidationService, + private accessPolicyService: AccessPolicyService + ) {} ngOnInit(): void { this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts index 440e88864b..35a5dc8773 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts @@ -1,10 +1,11 @@ import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatestWith, Observable, startWith, switchMap } from "rxjs"; +import { combineLatest, Observable, startWith, switchMap } from "rxjs"; import { DialogService } from "@bitwarden/components"; import { ProjectListView } from "../../models/view/project-list.view"; +import { AccessPolicyService } from "../../shared/access-policies/access-policy.service"; import { ProjectDeleteDialogComponent, ProjectDeleteOperation, @@ -29,14 +30,17 @@ export class ProjectsComponent implements OnInit { constructor( private route: ActivatedRoute, private projectService: ProjectService, + private accessPolicyService: AccessPolicyService, private dialogService: DialogService ) {} ngOnInit() { - this.projects$ = this.projectService.project$.pipe( - startWith(null), - combineLatestWith(this.route.params), - switchMap(async ([_, params]) => { + this.projects$ = combineLatest([ + this.route.params, + this.projectService.project$.pipe(startWith(null)), + this.accessPolicyService.projectAccessPolicyChanges$.pipe(startWith(null)), + ]).pipe( + switchMap(async ([params]) => { this.organizationId = params.organizationId; return await this.getProjects(); }) diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html index 2eb41bce21..3d896e6225 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html @@ -10,6 +10,7 @@ [columnTitle]="'groupSlashUser' | i18n" [emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n" (onCreateAccessPolicies)="handleCreateAccessPolicies($event)" + (onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" > diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts index 302bbd1f35..e539e0bf08 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts @@ -1,7 +1,19 @@ import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatestWith, map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs"; +import { + combineLatestWith, + map, + Observable, + share, + startWith, + Subject, + switchMap, + takeUntil, +} from "rxjs"; +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { ValidationService } from "@bitwarden/common/abstractions/validation.service"; +import { DialogService, SimpleDialogOptions, SimpleDialogType } from "@bitwarden/components"; import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view"; import { @@ -14,6 +26,10 @@ import { AccessSelectorComponent, AccessSelectorRowView, } from "../../shared/access-policies/access-selector.component"; +import { + AccessRemovalDetails, + AccessRemovalDialogComponent, +} from "../../shared/access-policies/dialogs/access-removal-dialog.component"; @Component({ selector: "sm-service-account-people", @@ -22,6 +38,8 @@ import { export class ServiceAccountPeopleComponent { private destroy$ = new Subject(); private serviceAccountId: string; + private organizationId: string; + private rows: AccessSelectorRowView[]; protected rows$: Observable = this.accessPolicyService.serviceAccountAccessPolicyChanges$.pipe( @@ -40,6 +58,7 @@ export class ServiceAccountPeopleComponent { accessPolicyId: policy.id, read: policy.read, write: policy.write, + userId: policy.userId, icon: AccessSelectorComponent.userIcon, static: true, }); @@ -53,13 +72,15 @@ export class ServiceAccountPeopleComponent { accessPolicyId: policy.id, read: policy.read, write: policy.write, + currentUserInGroup: policy.currentUserInGroup, icon: AccessSelectorComponent.groupIcon, static: true, }); }); return rows; - }) + }), + share() ); protected handleCreateAccessPolicies(selected: SelectItemView[]) { @@ -92,11 +113,49 @@ export class ServiceAccountPeopleComponent { ); } - constructor(private route: ActivatedRoute, private accessPolicyService: AccessPolicyService) {} + protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) { + if ( + await this.accessPolicyService.needToShowAccessRemovalWarning( + this.organizationId, + policy, + this.rows + ) + ) { + this.launchDeleteWarningDialog(policy); + return; + } + + try { + await this.accessPolicyService.deleteAccessPolicy(policy.accessPolicyId); + const simpleDialogOpts: SimpleDialogOptions = { + title: this.i18nService.t("saPeopleWarningTitle"), + content: this.i18nService.t("saPeopleWarningMessage"), + type: SimpleDialogType.WARNING, + acceptButtonText: this.i18nService.t("close"), + cancelButtonText: null, + }; + this.dialogService.openSimpleDialog(simpleDialogOpts); + } catch (e) { + this.validationService.showError(e); + } + } + + constructor( + private route: ActivatedRoute, + private dialogService: DialogService, + private i18nService: I18nService, + private validationService: ValidationService, + private accessPolicyService: AccessPolicyService + ) {} ngOnInit(): void { this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { this.serviceAccountId = params.serviceAccountId; + this.organizationId = params.organizationId; + }); + + this.rows$.pipe(takeUntil(this.destroy$)).subscribe((rows) => { + this.rows = rows; }); } @@ -104,4 +163,17 @@ export class ServiceAccountPeopleComponent { this.destroy$.next(); this.destroy$.complete(); } + + private launchDeleteWarningDialog(policy: AccessSelectorRowView) { + this.dialogService.open(AccessRemovalDialogComponent, { + data: { + title: "smAccessRemovalWarningSaTitle", + message: "smAccessRemovalWarningSaMessage", + operation: "delete", + type: "service-account", + returnRoute: ["sm", this.organizationId, "service-accounts"], + policy, + }, + }); + } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html index 6ceb2dc5f8..8ffc465e1b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html @@ -10,6 +10,7 @@ [columnTitle]="'projects' | i18n" [emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n" (onCreateAccessPolicies)="handleCreateAccessPolicies($event)" + (onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" > diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts index c886523834..87e8808279 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts @@ -2,6 +2,7 @@ import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { combineLatestWith, map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs"; +import { ValidationService } from "@bitwarden/common/abstractions/validation.service"; import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view"; import { ServiceAccountProjectAccessPolicyView } from "../../models/view/access-policy.view"; @@ -62,7 +63,19 @@ export class ServiceAccountProjectsComponent { ); } - constructor(private route: ActivatedRoute, private accessPolicyService: AccessPolicyService) {} + protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) { + try { + await this.accessPolicyService.deleteAccessPolicy(policy.accessPolicyId); + } catch (e) { + this.validationService.showError(e); + } + } + + constructor( + private route: ActivatedRoute, + private validationService: ValidationService, + private accessPolicyService: AccessPolicyService + ) {} ngOnInit(): void { this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts index 5c24559c10..8d82ab111b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts @@ -1,10 +1,11 @@ import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatestWith, Observable, startWith, switchMap } from "rxjs"; +import { combineLatest, Observable, startWith, switchMap } from "rxjs"; import { DialogService } from "@bitwarden/components"; import { ServiceAccountView } from "../models/view/service-account.view"; +import { AccessPolicyService } from "../shared/access-policies/access-policy.service"; import { ServiceAccountDialogComponent, @@ -24,14 +25,17 @@ export class ServiceAccountsComponent implements OnInit { constructor( private route: ActivatedRoute, private dialogService: DialogService, + private accessPolicyService: AccessPolicyService, private serviceAccountService: ServiceAccountService ) {} ngOnInit() { - this.serviceAccounts$ = this.serviceAccountService.serviceAccount$.pipe( - startWith(null), - combineLatestWith(this.route.params), - switchMap(async ([_, params]) => { + this.serviceAccounts$ = combineLatest([ + this.route.params, + this.serviceAccountService.serviceAccount$.pipe(startWith(null)), + this.accessPolicyService.serviceAccountAccessPolicyChanges$.pipe(startWith(null)), + ]).pipe( + switchMap(async ([params]) => { this.organizationId = params.organizationId; return await this.getServiceAccounts(); }) 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 index c99161b8a1..2adfc3b3b2 100644 --- 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 @@ -4,6 +4,7 @@ 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 { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; 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"; @@ -23,6 +24,7 @@ import { AccessPoliciesCreateRequest } from "../../shared/access-policies/models import { ProjectAccessPoliciesResponse } from "../../shared/access-policies/models/responses/project-access-policies.response"; import { ServiceAccountAccessPoliciesResponse } from "../../shared/access-policies/models/responses/service-accounts-access-policies.response"; +import { AccessSelectorRowView } from "./access-selector.component"; import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request"; import { AccessPolicyRequest } from "./models/requests/access-policy.request"; import { GrantedPolicyRequest } from "./models/requests/granted-policy.request"; @@ -64,10 +66,19 @@ export class AccessPolicyService { constructor( private cryptoService: CryptoService, + private organizationService: OrganizationService, protected apiService: ApiService, protected encryptService: EncryptService ) {} + refreshProjectAccessPolicyChanges() { + this._projectAccessPolicyChanges$.next(null); + } + + refreshServiceAccountAccessPolicyChanges() { + this._serviceAccountAccessPolicyChanges$.next(null); + } + async getGrantedPolicies( serviceAccountId: string, organizationId: string @@ -196,6 +207,36 @@ export class AccessPolicyService { ); } + async needToShowAccessRemovalWarning( + organizationId: string, + policy: AccessSelectorRowView, + currentPolicies: AccessSelectorRowView[] + ): Promise { + const organization = this.organizationService.get(organizationId); + if (organization.isOwner || organization.isAdmin) { + return false; + } + const currentUserId = organization.userId; + const readWriteGroupPolicies = currentPolicies + .filter((x) => x.accessPolicyId != policy.accessPolicyId) + .filter((x) => x.currentUserInGroup && x.read && x.write).length; + const readWriteUserPolicies = currentPolicies + .filter((x) => x.accessPolicyId != policy.accessPolicyId) + .filter((x) => x.userId == currentUserId && x.read && x.write).length; + + if (policy.type === "user" && policy.userId == currentUserId && readWriteGroupPolicies == 0) { + return true; + } else if ( + policy.type === "group" && + policy.currentUserInGroup && + readWriteUserPolicies == 0 && + readWriteGroupPolicies == 0 + ) { + return true; + } + return false; + } + private async createProjectAccessPoliciesView( organizationId: string, projectAccessPoliciesResponse: ProjectAccessPoliciesResponse @@ -255,6 +296,7 @@ export class AccessPolicyService { grantedProjectId: response.grantedProjectId, organizationUserId: response.organizationUserId, organizationUserName: response.organizationUserName, + userId: response.userId, }; } @@ -266,6 +308,7 @@ export class AccessPolicyService { grantedProjectId: response.grantedProjectId, groupId: response.groupId, groupName: response.groupName, + currentUserInGroup: response.currentUserInGroup, }; } @@ -335,6 +378,7 @@ export class AccessPolicyService { grantedServiceAccountId: response.grantedServiceAccountId, organizationUserId: response.organizationUserId, organizationUserName: response.organizationUserName, + userId: response.userId, }; } @@ -346,6 +390,7 @@ export class AccessPolicyService { grantedServiceAccountId: response.grantedServiceAccountId, groupId: response.groupId, groupName: response.groupName, + currentUserInGroup: response.currentUserInGroup, }; } @@ -471,14 +516,18 @@ export class AccessPolicyService { view.revisionDate = response.revisionDate; view.serviceAccountId = response.serviceAccountId; view.grantedProjectId = response.grantedProjectId; - view.serviceAccountName = await this.encryptService.decryptToUtf8( - new EncString(response.serviceAccountName), - orgKey - ); - view.grantedProjectName = await this.encryptService.decryptToUtf8( - new EncString(response.grantedProjectName), - orgKey - ); + 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; }) ); 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 index cef0f58244..9da038a2d8 100644 --- 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 @@ -35,11 +35,7 @@ *ngIf="!row.static; else staticPermissions" class="tw-mb-auto tw-inline-block tw-w-auto" > - @@ -62,7 +58,7 @@ size="default" [attr.title]="'remove' | i18n" [attr.aria-label]="'remove' | i18n" - [bitAction]="delete(row.accessPolicyId)" + [bitAction]="delete(row)" > 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 index 89784e81fc..7776c35be5 100644 --- 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 @@ -12,7 +12,6 @@ import { tap, } from "rxjs"; -import { ValidationService } from "@bitwarden/common/abstractions/validation.service"; import { Utils } from "@bitwarden/common/misc/utils"; import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view"; @@ -28,6 +27,8 @@ export type AccessSelectorRowView = { read: boolean; write: boolean; icon: string; + userId?: string; + currentUserInGroup?: boolean; static?: boolean; }; @@ -41,7 +42,12 @@ export class AccessSelectorComponent implements OnInit { static readonly serviceAccountIcon = "bwi-wrench"; static readonly projectIcon = "bwi-collection"; + /** + * Emits the selected itemss on submit. + */ @Output() onCreateAccessPolicies = new EventEmitter(); + @Output() onDeleteAccessPolicy = new EventEmitter(); + @Output() onUpdateAccessPolicy = new EventEmitter(); @Input() label: string; @Input() hint: string; @@ -105,11 +111,7 @@ export class AccessSelectorComponent implements OnInit { share() ); - constructor( - private accessPolicyService: AccessPolicyService, - private validationService: ValidationService, - private route: ActivatedRoute - ) {} + constructor(private accessPolicyService: AccessPolicyService, private route: ActivatedRoute) {} ngOnInit(): void { this.formGroup.disable(); @@ -128,28 +130,21 @@ export class AccessSelectorComponent implements OnInit { return firstValueFrom(this.selectItems$); }; - async update(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 === "canReadWrite") { - accessPolicyView.read = true; - accessPolicyView.write = true; - } - - await this.accessPolicyService.updateAccessPolicy(accessPolicyView); - } catch (e) { - this.validationService.showError(e); + async update(target: any, row: AccessSelectorRowView): Promise { + if (target.value === "canRead") { + row.read = true; + row.write = false; + } else if (target.value === "canReadWrite") { + row.read = true; + row.write = true; } + this.onUpdateAccessPolicy.emit(row); } - delete = (accessPolicyId: string) => async () => { + delete = (row: AccessSelectorRowView) => async () => { this.loading = true; this.formGroup.disable(); - await this.accessPolicyService.deleteAccessPolicy(accessPolicyId); + this.onDeleteAccessPolicy.emit(row); return firstValueFrom(this.selectItems$); }; @@ -176,4 +171,12 @@ export class AccessSelectorComponent implements OnInit { return "project"; } } + + static getBaseAccessPolicyView(row: AccessSelectorRowView) { + const view = new BaseAccessPolicyView(); + view.id = row.accessPolicyId; + view.read = row.read; + view.write = row.write; + return view; + } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.html new file mode 100644 index 0000000000..72f360523a --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.html @@ -0,0 +1,14 @@ + + {{ data.title | i18n }} + + {{ data.message | i18n }} + +
+ + +
+
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.ts new file mode 100644 index 0000000000..ecf80041ee --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.ts @@ -0,0 +1,65 @@ +import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { Component, Inject, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; + +import { AccessPolicyService } from "../access-policy.service"; +import { AccessSelectorComponent, AccessSelectorRowView } from "../access-selector.component"; + +export interface AccessRemovalDetails { + title: string; + message: string; + operation: "update" | "delete"; + type: "project" | "service-account"; + returnRoute: string[]; + policy: AccessSelectorRowView; +} + +@Component({ + selector: "sm-access-removal-dialog", + templateUrl: "./access-removal-dialog.component.html", +}) +export class AccessRemovalDialogComponent implements OnInit { + constructor( + public dialogRef: DialogRef, + private router: Router, + private accessPolicyService: AccessPolicyService, + @Inject(DIALOG_DATA) public data: AccessRemovalDetails + ) {} + + ngOnInit(): void { + // TODO remove null checks once strictNullChecks in TypeScript is turned on. + if ( + !this.data.message || + !this.data.title || + !this.data.operation || + !this.data.returnRoute || + !this.data.policy + ) { + this.dialogRef.close(); + throw new Error( + "The access removal dialog was not called with the appropriate operation values." + ); + } + } + + removeAccess = async () => { + await this.router.navigate(this.data.returnRoute); + if (this.data.operation === "delete") { + await this.accessPolicyService.deleteAccessPolicy(this.data.policy.accessPolicyId); + } else if (this.data.operation == "update") { + await this.accessPolicyService.updateAccessPolicy( + AccessSelectorComponent.getBaseAccessPolicyView(this.data.policy) + ); + } + this.dialogRef.close(); + }; + + cancel = () => { + if (this.data.type == "project") { + this.accessPolicyService.refreshProjectAccessPolicyChanges(); + } else if (this.data.type == "service-account") { + this.accessPolicyService.refreshServiceAccountAccessPolicyChanges(); + } + this.dialogRef.close(); + }; +} 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 index 58f574f4ad..d17b22dc86 100644 --- 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 @@ -21,12 +21,14 @@ export class UserProjectAccessPolicyResponse extends BaseAccessPolicyResponse { organizationUserId: string; organizationUserName: string; grantedProjectId: string; + userId: string; constructor(response: any) { super(response); this.organizationUserId = this.getResponseProperty("OrganizationUserId"); this.organizationUserName = this.getResponseProperty("OrganizationUserName"); this.grantedProjectId = this.getResponseProperty("GrantedProjectId"); + this.userId = this.getResponseProperty("UserId"); } } @@ -34,12 +36,14 @@ export class UserServiceAccountAccessPolicyResponse extends BaseAccessPolicyResp organizationUserId: string; organizationUserName: string; grantedServiceAccountId: string; + userId: string; constructor(response: any) { super(response); this.organizationUserId = this.getResponseProperty("OrganizationUserId"); this.organizationUserName = this.getResponseProperty("OrganizationUserName"); this.grantedServiceAccountId = this.getResponseProperty("GrantedServiceAccountId"); + this.userId = this.getResponseProperty("UserId"); } } @@ -47,12 +51,14 @@ export class GroupProjectAccessPolicyResponse extends BaseAccessPolicyResponse { groupId: string; groupName: string; grantedProjectId: string; + currentUserInGroup: boolean; constructor(response: any) { super(response); this.groupId = this.getResponseProperty("GroupId"); this.groupName = this.getResponseProperty("GroupName"); this.grantedProjectId = this.getResponseProperty("GrantedProjectId"); + this.currentUserInGroup = this.getResponseProperty("CurrentUserInGroup"); } } @@ -60,12 +66,14 @@ export class GroupServiceAccountAccessPolicyResponse extends BaseAccessPolicyRes groupId: string; groupName: string; grantedServiceAccountId: string; + currentUserInGroup: boolean; constructor(response: any) { super(response); this.groupId = this.getResponseProperty("GroupId"); this.groupName = this.getResponseProperty("GroupName"); this.grantedServiceAccountId = this.getResponseProperty("GrantedServiceAccountId"); + this.currentUserInGroup = this.getResponseProperty("CurrentUserInGroup"); } } 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 129016c947..9361c1ab03 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 @@ -6,6 +6,7 @@ import { CoreOrganizationModule } from "@bitwarden/web-vault/app/organizations/c import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { AccessSelectorComponent } from "./access-policies/access-selector.component"; +import { AccessRemovalDialogComponent } from "./access-policies/dialogs/access-removal-dialog.component"; import { BulkStatusDialogComponent } from "./dialogs/bulk-status-dialog.component"; import { HeaderComponent } from "./header.component"; import { NewMenuComponent } from "./new-menu.component"; @@ -17,6 +18,7 @@ import { SecretsListComponent } from "./secrets-list.component"; imports: [SharedModule, ProductSwitcherModule, MultiSelectModule, CoreOrganizationModule], exports: [ SharedModule, + AccessRemovalDialogComponent, BulkStatusDialogComponent, HeaderComponent, NewMenuComponent, @@ -26,6 +28,7 @@ import { SecretsListComponent } from "./secrets-list.component"; AccessSelectorComponent, ], declarations: [ + AccessRemovalDialogComponent, BulkStatusDialogComponent, HeaderComponent, NewMenuComponent,