mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-21 16:18:28 +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:
parent
f717c3d619
commit
c711312fee
@ -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"
|
||||
},
|
||||
|
@ -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 {
|
||||
|
@ -10,6 +10,8 @@
|
||||
[columnTitle]="'groupSlashUser' | i18n"
|
||||
[emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n"
|
||||
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)"
|
||||
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)"
|
||||
(onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)"
|
||||
>
|
||||
</sm-access-selector>
|
||||
</div>
|
||||
|
@ -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<void>();
|
||||
private organizationId: string;
|
||||
private projectId: string;
|
||||
private rows: AccessSelectorRowView[];
|
||||
|
||||
protected rows$: Observable<AccessSelectorRowView[]> =
|
||||
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<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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
[columnTitle]="'serviceAccounts' | i18n"
|
||||
[emptyMessage]="'projectEmptyServiceAccountAccessPolicies' | i18n"
|
||||
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)"
|
||||
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)"
|
||||
>
|
||||
</sm-access-selector>
|
||||
</div>
|
||||
|
@ -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) => {
|
||||
|
@ -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();
|
||||
})
|
||||
|
@ -10,6 +10,7 @@
|
||||
[columnTitle]="'groupSlashUser' | i18n"
|
||||
[emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n"
|
||||
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)"
|
||||
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)"
|
||||
>
|
||||
</sm-access-selector>
|
||||
</div>
|
||||
|
@ -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<void>();
|
||||
private serviceAccountId: string;
|
||||
private organizationId: string;
|
||||
private rows: AccessSelectorRowView[];
|
||||
|
||||
protected rows$: Observable<AccessSelectorRowView[]> =
|
||||
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<unknown, AccessRemovalDetails>(AccessRemovalDialogComponent, {
|
||||
data: {
|
||||
title: "smAccessRemovalWarningSaTitle",
|
||||
message: "smAccessRemovalWarningSaMessage",
|
||||
operation: "delete",
|
||||
type: "service-account",
|
||||
returnRoute: ["sm", this.organizationId, "service-accounts"],
|
||||
policy,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
[columnTitle]="'projects' | i18n"
|
||||
[emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n"
|
||||
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)"
|
||||
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)"
|
||||
>
|
||||
</sm-access-selector>
|
||||
</div>
|
||||
|
@ -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) => {
|
||||
|
@ -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();
|
||||
})
|
||||
|
@ -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<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(
|
||||
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;
|
||||
})
|
||||
);
|
||||
|
@ -35,11 +35,7 @@
|
||||
*ngIf="!row.static; else staticPermissions"
|
||||
class="tw-mb-auto tw-inline-block tw-w-auto"
|
||||
>
|
||||
<select
|
||||
bitInput
|
||||
(change)="update($event.target, row.accessPolicyId)"
|
||||
[disabled]="row.static"
|
||||
>
|
||||
<select bitInput (change)="update($event.target, row)" [disabled]="row.static">
|
||||
<option value="canRead" [selected]="row.read && row.write != true">
|
||||
{{ "canRead" | i18n }}
|
||||
</option>
|
||||
@ -62,7 +58,7 @@
|
||||
size="default"
|
||||
[attr.title]="'remove' | i18n"
|
||||
[attr.aria-label]="'remove' | i18n"
|
||||
[bitAction]="delete(row.accessPolicyId)"
|
||||
[bitAction]="delete(row)"
|
||||
></button>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -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<SelectItemView[]>();
|
||||
@Output() onDeleteAccessPolicy = new EventEmitter<AccessSelectorRowView>();
|
||||
@Output() onUpdateAccessPolicy = new EventEmitter<AccessSelectorRowView>();
|
||||
|
||||
@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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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();
|
||||
};
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user