1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-22 16:29:09 +01:00

[SM-581] User access removal warnings (#4904)

* init refactor

* Fix current user access checks

* Add in warning dialogs that are aware of other APs

* cleanup handlers; refresh sa list on removal

* Code review updates

* [SM-580] Add warning dialog for Service account People tab (#4893)

* Add warning dialog from figma

* move dialog out of access selector component; add after delete event; remove people-sa logic

* remove commented code and unused service

* Updates to work with SM-581

---------

Co-authored-by: William Martin <contact@willmartian.com>

---------

Co-authored-by: William Martin <contact@willmartian.com>
This commit is contained in:
Thomas Avery 2023-03-06 11:32:02 -06:00 committed by GitHub
parent f717c3d619
commit c711312fee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 416 additions and 56 deletions

View File

@ -6570,6 +6570,27 @@
"secretsManagerEnable": { "secretsManagerEnable": {
"message": "Enable Secrets Manager Beta" "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": { "checkForBreaches": {
"message": "Check known data breaches for this password" "message": "Check known data breaches for this password"
}, },

View File

@ -10,24 +10,28 @@ export class UserProjectAccessPolicyView extends BaseAccessPolicyView {
organizationUserId: string; organizationUserId: string;
organizationUserName: string; organizationUserName: string;
grantedProjectId: string; grantedProjectId: string;
userId: string;
} }
export class UserServiceAccountAccessPolicyView extends BaseAccessPolicyView { export class UserServiceAccountAccessPolicyView extends BaseAccessPolicyView {
organizationUserId: string; organizationUserId: string;
organizationUserName: string; organizationUserName: string;
grantedServiceAccountId: string; grantedServiceAccountId: string;
userId: string;
} }
export class GroupProjectAccessPolicyView extends BaseAccessPolicyView { export class GroupProjectAccessPolicyView extends BaseAccessPolicyView {
groupId: string; groupId: string;
groupName: string; groupName: string;
grantedProjectId: string; grantedProjectId: string;
currentUserInGroup: boolean;
} }
export class GroupServiceAccountAccessPolicyView extends BaseAccessPolicyView { export class GroupServiceAccountAccessPolicyView extends BaseAccessPolicyView {
groupId: string; groupId: string;
groupName: string; groupName: string;
grantedServiceAccountId: string; grantedServiceAccountId: string;
currentUserInGroup: boolean;
} }
export class ServiceAccountProjectAccessPolicyView extends BaseAccessPolicyView { export class ServiceAccountProjectAccessPolicyView extends BaseAccessPolicyView {

View File

@ -10,6 +10,8 @@
[columnTitle]="'groupSlashUser' | i18n" [columnTitle]="'groupSlashUser' | i18n"
[emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n" [emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n"
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)" (onCreateAccessPolicies)="handleCreateAccessPolicies($event)"
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)"
(onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)"
> >
</sm-access-selector> </sm-access-selector>
</div> </div>

View File

@ -1,8 +1,9 @@
import { Component, OnDestroy, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; 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 { import {
GroupProjectAccessPolicyView, GroupProjectAccessPolicyView,
@ -14,6 +15,10 @@ import {
AccessSelectorComponent, AccessSelectorComponent,
AccessSelectorRowView, AccessSelectorRowView,
} from "../../shared/access-policies/access-selector.component"; } from "../../shared/access-policies/access-selector.component";
import {
AccessRemovalDetails,
AccessRemovalDialogComponent,
} from "../../shared/access-policies/dialogs/access-removal-dialog.component";
@Component({ @Component({
selector: "sm-project-people", selector: "sm-project-people",
@ -23,6 +28,7 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private organizationId: string; private organizationId: string;
private projectId: string; private projectId: string;
private rows: AccessSelectorRowView[];
protected rows$: Observable<AccessSelectorRowView[]> = protected rows$: Observable<AccessSelectorRowView[]> =
this.accessPolicyService.projectAccessPolicyChanges$.pipe( this.accessPolicyService.projectAccessPolicyChanges$.pipe(
@ -40,6 +46,7 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy {
accessPolicyId: policy.id, accessPolicyId: policy.id,
read: policy.read, read: policy.read,
write: policy.write, write: policy.write,
userId: policy.userId,
icon: AccessSelectorComponent.userIcon, icon: AccessSelectorComponent.userIcon,
}); });
}); });
@ -52,11 +59,13 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy {
accessPolicyId: policy.id, accessPolicyId: policy.id,
read: policy.read, read: policy.read,
write: policy.write, write: policy.write,
currentUserInGroup: policy.currentUserInGroup,
icon: AccessSelectorComponent.groupIcon, icon: AccessSelectorComponent.groupIcon,
}); });
}); });
return rows; return rows;
}) }),
share()
); );
protected handleCreateAccessPolicies(selected: SelectItemView[]) { 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 { ngOnInit(): void {
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {
this.organizationId = params.organizationId; this.organizationId = params.organizationId;
this.projectId = params.projectId; this.projectId = params.projectId;
}); });
this.rows$.pipe(takeUntil(this.destroy$)).subscribe((rows) => {
this.rows = rows;
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
} }
private async launchDeleteWarningDialog(policy: AccessSelectorRowView) {
this.dialogService.open<unknown, AccessRemovalDetails>(AccessRemovalDialogComponent, {
data: {
title: "smAccessRemovalWarningProjectTitle",
message: "smAccessRemovalWarningProjectMessage",
operation: "delete",
type: "project",
returnRoute: ["sm", this.organizationId, "projects"],
policy,
},
});
}
private launchUpdateWarningDialog(policy: AccessSelectorRowView) {
this.dialogService.open<unknown, AccessRemovalDetails>(AccessRemovalDialogComponent, {
data: {
title: "smAccessRemovalWarningProjectTitle",
message: "smAccessRemovalWarningProjectMessage",
operation: "update",
type: "project",
returnRoute: ["sm", this.organizationId, "projects"],
policy,
},
});
}
} }

View File

@ -10,6 +10,7 @@
[columnTitle]="'serviceAccounts' | i18n" [columnTitle]="'serviceAccounts' | i18n"
[emptyMessage]="'projectEmptyServiceAccountAccessPolicies' | i18n" [emptyMessage]="'projectEmptyServiceAccountAccessPolicies' | i18n"
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)" (onCreateAccessPolicies)="handleCreateAccessPolicies($event)"
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)"
> >
</sm-access-selector> </sm-access-selector>
</div> </div>

View File

@ -2,6 +2,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs"; import { map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs";
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
import { SelectItemView } from "@bitwarden/components"; import { SelectItemView } from "@bitwarden/components";
import { 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 { ngOnInit(): void {
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {

View File

@ -1,10 +1,11 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; 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 { DialogService } from "@bitwarden/components";
import { ProjectListView } from "../../models/view/project-list.view"; import { ProjectListView } from "../../models/view/project-list.view";
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
import { import {
ProjectDeleteDialogComponent, ProjectDeleteDialogComponent,
ProjectDeleteOperation, ProjectDeleteOperation,
@ -29,14 +30,17 @@ export class ProjectsComponent implements OnInit {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private projectService: ProjectService, private projectService: ProjectService,
private accessPolicyService: AccessPolicyService,
private dialogService: DialogService private dialogService: DialogService
) {} ) {}
ngOnInit() { ngOnInit() {
this.projects$ = this.projectService.project$.pipe( this.projects$ = combineLatest([
startWith(null), this.route.params,
combineLatestWith(this.route.params), this.projectService.project$.pipe(startWith(null)),
switchMap(async ([_, params]) => { this.accessPolicyService.projectAccessPolicyChanges$.pipe(startWith(null)),
]).pipe(
switchMap(async ([params]) => {
this.organizationId = params.organizationId; this.organizationId = params.organizationId;
return await this.getProjects(); return await this.getProjects();
}) })

View File

@ -10,6 +10,7 @@
[columnTitle]="'groupSlashUser' | i18n" [columnTitle]="'groupSlashUser' | i18n"
[emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n" [emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n"
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)" (onCreateAccessPolicies)="handleCreateAccessPolicies($event)"
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)"
> >
</sm-access-selector> </sm-access-selector>
</div> </div>

View File

@ -1,7 +1,19 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; 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 { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view";
import { import {
@ -14,6 +26,10 @@ import {
AccessSelectorComponent, AccessSelectorComponent,
AccessSelectorRowView, AccessSelectorRowView,
} from "../../shared/access-policies/access-selector.component"; } from "../../shared/access-policies/access-selector.component";
import {
AccessRemovalDetails,
AccessRemovalDialogComponent,
} from "../../shared/access-policies/dialogs/access-removal-dialog.component";
@Component({ @Component({
selector: "sm-service-account-people", selector: "sm-service-account-people",
@ -22,6 +38,8 @@ import {
export class ServiceAccountPeopleComponent { export class ServiceAccountPeopleComponent {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private serviceAccountId: string; private serviceAccountId: string;
private organizationId: string;
private rows: AccessSelectorRowView[];
protected rows$: Observable<AccessSelectorRowView[]> = protected rows$: Observable<AccessSelectorRowView[]> =
this.accessPolicyService.serviceAccountAccessPolicyChanges$.pipe( this.accessPolicyService.serviceAccountAccessPolicyChanges$.pipe(
@ -40,6 +58,7 @@ export class ServiceAccountPeopleComponent {
accessPolicyId: policy.id, accessPolicyId: policy.id,
read: policy.read, read: policy.read,
write: policy.write, write: policy.write,
userId: policy.userId,
icon: AccessSelectorComponent.userIcon, icon: AccessSelectorComponent.userIcon,
static: true, static: true,
}); });
@ -53,13 +72,15 @@ export class ServiceAccountPeopleComponent {
accessPolicyId: policy.id, accessPolicyId: policy.id,
read: policy.read, read: policy.read,
write: policy.write, write: policy.write,
currentUserInGroup: policy.currentUserInGroup,
icon: AccessSelectorComponent.groupIcon, icon: AccessSelectorComponent.groupIcon,
static: true, static: true,
}); });
}); });
return rows; return rows;
}) }),
share()
); );
protected handleCreateAccessPolicies(selected: SelectItemView[]) { 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 { ngOnInit(): void {
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {
this.serviceAccountId = params.serviceAccountId; 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$.next();
this.destroy$.complete(); this.destroy$.complete();
} }
private launchDeleteWarningDialog(policy: AccessSelectorRowView) {
this.dialogService.open<unknown, AccessRemovalDetails>(AccessRemovalDialogComponent, {
data: {
title: "smAccessRemovalWarningSaTitle",
message: "smAccessRemovalWarningSaMessage",
operation: "delete",
type: "service-account",
returnRoute: ["sm", this.organizationId, "service-accounts"],
policy,
},
});
}
} }

View File

@ -10,6 +10,7 @@
[columnTitle]="'projects' | i18n" [columnTitle]="'projects' | i18n"
[emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n" [emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n"
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)" (onCreateAccessPolicies)="handleCreateAccessPolicies($event)"
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)"
> >
</sm-access-selector> </sm-access-selector>
</div> </div>

View File

@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { combineLatestWith, map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs"; 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 { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view";
import { ServiceAccountProjectAccessPolicyView } from "../../models/view/access-policy.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 { ngOnInit(): void {
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {

View File

@ -1,10 +1,11 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; 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 { DialogService } from "@bitwarden/components";
import { ServiceAccountView } from "../models/view/service-account.view"; import { ServiceAccountView } from "../models/view/service-account.view";
import { AccessPolicyService } from "../shared/access-policies/access-policy.service";
import { import {
ServiceAccountDialogComponent, ServiceAccountDialogComponent,
@ -24,14 +25,17 @@ export class ServiceAccountsComponent implements OnInit {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private dialogService: DialogService, private dialogService: DialogService,
private accessPolicyService: AccessPolicyService,
private serviceAccountService: ServiceAccountService private serviceAccountService: ServiceAccountService
) {} ) {}
ngOnInit() { ngOnInit() {
this.serviceAccounts$ = this.serviceAccountService.serviceAccount$.pipe( this.serviceAccounts$ = combineLatest([
startWith(null), this.route.params,
combineLatestWith(this.route.params), this.serviceAccountService.serviceAccount$.pipe(startWith(null)),
switchMap(async ([_, params]) => { this.accessPolicyService.serviceAccountAccessPolicyChanges$.pipe(startWith(null)),
]).pipe(
switchMap(async ([params]) => {
this.organizationId = params.organizationId; this.organizationId = params.organizationId;
return await this.getServiceAccounts(); return await this.getServiceAccounts();
}) })

View File

@ -4,6 +4,7 @@ import { Subject } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/abstractions/encrypt.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 { EncString } from "@bitwarden/common/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
import { ListResponse } from "@bitwarden/common/models/response/list.response"; 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 { 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 { 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 { 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 { GrantedPolicyRequest } from "./models/requests/granted-policy.request";
@ -64,10 +66,19 @@ export class AccessPolicyService {
constructor( constructor(
private cryptoService: CryptoService, private cryptoService: CryptoService,
private organizationService: OrganizationService,
protected apiService: ApiService, protected apiService: ApiService,
protected encryptService: EncryptService protected encryptService: EncryptService
) {} ) {}
refreshProjectAccessPolicyChanges() {
this._projectAccessPolicyChanges$.next(null);
}
refreshServiceAccountAccessPolicyChanges() {
this._serviceAccountAccessPolicyChanges$.next(null);
}
async getGrantedPolicies( async getGrantedPolicies(
serviceAccountId: string, serviceAccountId: string,
organizationId: string organizationId: string
@ -196,6 +207,36 @@ export class AccessPolicyService {
); );
} }
async needToShowAccessRemovalWarning(
organizationId: string,
policy: AccessSelectorRowView,
currentPolicies: AccessSelectorRowView[]
): Promise<boolean> {
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( private async createProjectAccessPoliciesView(
organizationId: string, organizationId: string,
projectAccessPoliciesResponse: ProjectAccessPoliciesResponse projectAccessPoliciesResponse: ProjectAccessPoliciesResponse
@ -255,6 +296,7 @@ export class AccessPolicyService {
grantedProjectId: response.grantedProjectId, grantedProjectId: response.grantedProjectId,
organizationUserId: response.organizationUserId, organizationUserId: response.organizationUserId,
organizationUserName: response.organizationUserName, organizationUserName: response.organizationUserName,
userId: response.userId,
}; };
} }
@ -266,6 +308,7 @@ export class AccessPolicyService {
grantedProjectId: response.grantedProjectId, grantedProjectId: response.grantedProjectId,
groupId: response.groupId, groupId: response.groupId,
groupName: response.groupName, groupName: response.groupName,
currentUserInGroup: response.currentUserInGroup,
}; };
} }
@ -335,6 +378,7 @@ export class AccessPolicyService {
grantedServiceAccountId: response.grantedServiceAccountId, grantedServiceAccountId: response.grantedServiceAccountId,
organizationUserId: response.organizationUserId, organizationUserId: response.organizationUserId,
organizationUserName: response.organizationUserName, organizationUserName: response.organizationUserName,
userId: response.userId,
}; };
} }
@ -346,6 +390,7 @@ export class AccessPolicyService {
grantedServiceAccountId: response.grantedServiceAccountId, grantedServiceAccountId: response.grantedServiceAccountId,
groupId: response.groupId, groupId: response.groupId,
groupName: response.groupName, groupName: response.groupName,
currentUserInGroup: response.currentUserInGroup,
}; };
} }
@ -471,14 +516,18 @@ export class AccessPolicyService {
view.revisionDate = response.revisionDate; view.revisionDate = response.revisionDate;
view.serviceAccountId = response.serviceAccountId; view.serviceAccountId = response.serviceAccountId;
view.grantedProjectId = response.grantedProjectId; view.grantedProjectId = response.grantedProjectId;
view.serviceAccountName = await this.encryptService.decryptToUtf8( view.serviceAccountName = response.serviceAccountName
? await this.encryptService.decryptToUtf8(
new EncString(response.serviceAccountName), new EncString(response.serviceAccountName),
orgKey orgKey
); )
view.grantedProjectName = await this.encryptService.decryptToUtf8( : null;
view.grantedProjectName = response.grantedProjectName
? await this.encryptService.decryptToUtf8(
new EncString(response.grantedProjectName), new EncString(response.grantedProjectName),
orgKey orgKey
); )
: null;
return view; return view;
}) })
); );

View File

@ -35,11 +35,7 @@
*ngIf="!row.static; else staticPermissions" *ngIf="!row.static; else staticPermissions"
class="tw-mb-auto tw-inline-block tw-w-auto" class="tw-mb-auto tw-inline-block tw-w-auto"
> >
<select <select bitInput (change)="update($event.target, row)" [disabled]="row.static">
bitInput
(change)="update($event.target, row.accessPolicyId)"
[disabled]="row.static"
>
<option value="canRead" [selected]="row.read && row.write != true"> <option value="canRead" [selected]="row.read && row.write != true">
{{ "canRead" | i18n }} {{ "canRead" | i18n }}
</option> </option>
@ -62,7 +58,7 @@
size="default" size="default"
[attr.title]="'remove' | i18n" [attr.title]="'remove' | i18n"
[attr.aria-label]="'remove' | i18n" [attr.aria-label]="'remove' | i18n"
[bitAction]="delete(row.accessPolicyId)" [bitAction]="delete(row)"
></button> ></button>
</td> </td>
</tr> </tr>

View File

@ -12,7 +12,6 @@ import {
tap, tap,
} from "rxjs"; } from "rxjs";
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
import { Utils } from "@bitwarden/common/misc/utils"; import { Utils } from "@bitwarden/common/misc/utils";
import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view"; import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view";
@ -28,6 +27,8 @@ export type AccessSelectorRowView = {
read: boolean; read: boolean;
write: boolean; write: boolean;
icon: string; icon: string;
userId?: string;
currentUserInGroup?: boolean;
static?: boolean; static?: boolean;
}; };
@ -41,7 +42,12 @@ export class AccessSelectorComponent implements OnInit {
static readonly serviceAccountIcon = "bwi-wrench"; static readonly serviceAccountIcon = "bwi-wrench";
static readonly projectIcon = "bwi-collection"; static readonly projectIcon = "bwi-collection";
/**
* Emits the selected itemss on submit.
*/
@Output() onCreateAccessPolicies = new EventEmitter<SelectItemView[]>(); @Output() onCreateAccessPolicies = new EventEmitter<SelectItemView[]>();
@Output() onDeleteAccessPolicy = new EventEmitter<AccessSelectorRowView>();
@Output() onUpdateAccessPolicy = new EventEmitter<AccessSelectorRowView>();
@Input() label: string; @Input() label: string;
@Input() hint: string; @Input() hint: string;
@ -105,11 +111,7 @@ export class AccessSelectorComponent implements OnInit {
share() share()
); );
constructor( constructor(private accessPolicyService: AccessPolicyService, private route: ActivatedRoute) {}
private accessPolicyService: AccessPolicyService,
private validationService: ValidationService,
private route: ActivatedRoute
) {}
ngOnInit(): void { ngOnInit(): void {
this.formGroup.disable(); this.formGroup.disable();
@ -128,28 +130,21 @@ export class AccessSelectorComponent implements OnInit {
return firstValueFrom(this.selectItems$); return firstValueFrom(this.selectItems$);
}; };
async update(target: any, accessPolicyId: string): Promise<void> { async update(target: any, row: AccessSelectorRowView): Promise<void> {
try {
const accessPolicyView = new BaseAccessPolicyView();
accessPolicyView.id = accessPolicyId;
if (target.value === "canRead") { if (target.value === "canRead") {
accessPolicyView.read = true; row.read = true;
accessPolicyView.write = false; row.write = false;
} else if (target.value === "canReadWrite") { } else if (target.value === "canReadWrite") {
accessPolicyView.read = true; row.read = true;
accessPolicyView.write = true; row.write = true;
}
this.onUpdateAccessPolicy.emit(row);
} }
await this.accessPolicyService.updateAccessPolicy(accessPolicyView); delete = (row: AccessSelectorRowView) => async () => {
} catch (e) {
this.validationService.showError(e);
}
}
delete = (accessPolicyId: string) => async () => {
this.loading = true; this.loading = true;
this.formGroup.disable(); this.formGroup.disable();
await this.accessPolicyService.deleteAccessPolicy(accessPolicyId); this.onDeleteAccessPolicy.emit(row);
return firstValueFrom(this.selectItems$); return firstValueFrom(this.selectItems$);
}; };
@ -176,4 +171,12 @@ export class AccessSelectorComponent implements OnInit {
return "project"; return "project";
} }
} }
static getBaseAccessPolicyView(row: AccessSelectorRowView) {
const view = new BaseAccessPolicyView();
view.id = row.accessPolicyId;
view.read = row.read;
view.write = row.write;
return view;
}
} }

View File

@ -0,0 +1,14 @@
<bit-simple-dialog>
<span bitDialogTitle>{{ data.title | i18n }}</span>
<span bitDialogContent>
{{ data.message | i18n }}
</span>
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button type="button" bitButton buttonType="danger" [bitAction]="removeAccess">
{{ "removeAccess" | i18n }}
</button>
<button type="button" bitButton buttonType="secondary" [bitAction]="cancel">
{{ "cancel" | i18n }}
</button>
</div>
</bit-simple-dialog>

View File

@ -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();
};
}

View File

@ -21,12 +21,14 @@ export class UserProjectAccessPolicyResponse extends BaseAccessPolicyResponse {
organizationUserId: string; organizationUserId: string;
organizationUserName: string; organizationUserName: string;
grantedProjectId: string; grantedProjectId: string;
userId: string;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
this.organizationUserId = this.getResponseProperty("OrganizationUserId"); this.organizationUserId = this.getResponseProperty("OrganizationUserId");
this.organizationUserName = this.getResponseProperty("OrganizationUserName"); this.organizationUserName = this.getResponseProperty("OrganizationUserName");
this.grantedProjectId = this.getResponseProperty("GrantedProjectId"); this.grantedProjectId = this.getResponseProperty("GrantedProjectId");
this.userId = this.getResponseProperty("UserId");
} }
} }
@ -34,12 +36,14 @@ export class UserServiceAccountAccessPolicyResponse extends BaseAccessPolicyResp
organizationUserId: string; organizationUserId: string;
organizationUserName: string; organizationUserName: string;
grantedServiceAccountId: string; grantedServiceAccountId: string;
userId: string;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
this.organizationUserId = this.getResponseProperty("OrganizationUserId"); this.organizationUserId = this.getResponseProperty("OrganizationUserId");
this.organizationUserName = this.getResponseProperty("OrganizationUserName"); this.organizationUserName = this.getResponseProperty("OrganizationUserName");
this.grantedServiceAccountId = this.getResponseProperty("GrantedServiceAccountId"); this.grantedServiceAccountId = this.getResponseProperty("GrantedServiceAccountId");
this.userId = this.getResponseProperty("UserId");
} }
} }
@ -47,12 +51,14 @@ export class GroupProjectAccessPolicyResponse extends BaseAccessPolicyResponse {
groupId: string; groupId: string;
groupName: string; groupName: string;
grantedProjectId: string; grantedProjectId: string;
currentUserInGroup: boolean;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
this.groupId = this.getResponseProperty("GroupId"); this.groupId = this.getResponseProperty("GroupId");
this.groupName = this.getResponseProperty("GroupName"); this.groupName = this.getResponseProperty("GroupName");
this.grantedProjectId = this.getResponseProperty("GrantedProjectId"); this.grantedProjectId = this.getResponseProperty("GrantedProjectId");
this.currentUserInGroup = this.getResponseProperty("CurrentUserInGroup");
} }
} }
@ -60,12 +66,14 @@ export class GroupServiceAccountAccessPolicyResponse extends BaseAccessPolicyRes
groupId: string; groupId: string;
groupName: string; groupName: string;
grantedServiceAccountId: string; grantedServiceAccountId: string;
currentUserInGroup: boolean;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
this.groupId = this.getResponseProperty("GroupId"); this.groupId = this.getResponseProperty("GroupId");
this.groupName = this.getResponseProperty("GroupName"); this.groupName = this.getResponseProperty("GroupName");
this.grantedServiceAccountId = this.getResponseProperty("GrantedServiceAccountId"); this.grantedServiceAccountId = this.getResponseProperty("GrantedServiceAccountId");
this.currentUserInGroup = this.getResponseProperty("CurrentUserInGroup");
} }
} }

View File

@ -6,6 +6,7 @@ import { CoreOrganizationModule } from "@bitwarden/web-vault/app/organizations/c
import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { AccessSelectorComponent } from "./access-policies/access-selector.component"; 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 { BulkStatusDialogComponent } from "./dialogs/bulk-status-dialog.component";
import { HeaderComponent } from "./header.component"; import { HeaderComponent } from "./header.component";
import { NewMenuComponent } from "./new-menu.component"; import { NewMenuComponent } from "./new-menu.component";
@ -17,6 +18,7 @@ import { SecretsListComponent } from "./secrets-list.component";
imports: [SharedModule, ProductSwitcherModule, MultiSelectModule, CoreOrganizationModule], imports: [SharedModule, ProductSwitcherModule, MultiSelectModule, CoreOrganizationModule],
exports: [ exports: [
SharedModule, SharedModule,
AccessRemovalDialogComponent,
BulkStatusDialogComponent, BulkStatusDialogComponent,
HeaderComponent, HeaderComponent,
NewMenuComponent, NewMenuComponent,
@ -26,6 +28,7 @@ import { SecretsListComponent } from "./secrets-list.component";
AccessSelectorComponent, AccessSelectorComponent,
], ],
declarations: [ declarations: [
AccessRemovalDialogComponent,
BulkStatusDialogComponent, BulkStatusDialogComponent,
HeaderComponent, HeaderComponent,
NewMenuComponent, NewMenuComponent,