[AC-2171] Member modal - limit admin access - editing self (#8299)

* If editing your own member modal, you cannot add new collections or groups
  * Update forms to prevent this
  * Add helper text
* Delete unused api method
This commit is contained in:
Thomas Rittson 2024-03-15 09:33:15 +10:00 committed by GitHub
parent 1d76e80afb
commit 5506842623
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 67 additions and 39 deletions

View File

@ -399,7 +399,11 @@
</bit-tab> </bit-tab>
<bit-tab *ngIf="organization.useGroups" [label]="'groups' | i18n"> <bit-tab *ngIf="organization.useGroups" [label]="'groups' | i18n">
<div class="tw-mb-6"> <div class="tw-mb-6">
{{ "groupAccessUserDesc" | i18n }} {{
(restrictedAccess$ | async)
? ("restrictedGroupAccess" | i18n)
: ("groupAccessUserDesc" | i18n)
}}
</div> </div>
<bit-access-selector <bit-access-selector
formControlName="groups" formControlName="groups"
@ -408,10 +412,14 @@
[selectorLabelText]="'selectGroups' | i18n" [selectorLabelText]="'selectGroups' | i18n"
[emptySelectionText]="'noGroupsAdded' | i18n" [emptySelectionText]="'noGroupsAdded' | i18n"
[flexibleCollectionsEnabled]="organization.flexibleCollections" [flexibleCollectionsEnabled]="organization.flexibleCollections"
[hideMultiSelect]="restrictedAccess$ | async"
></bit-access-selector> ></bit-access-selector>
</bit-tab> </bit-tab>
<bit-tab [label]="'collections' | i18n"> <bit-tab [label]="'collections' | i18n">
<div *ngIf="organization.useGroups" class="tw-mb-6"> <div class="tw-mb-6" *ngIf="restrictedAccess$ | async">
{{ "restrictedCollectionAccess" | i18n }}
</div>
<div *ngIf="organization.useGroups && !(restrictedAccess$ | async)" class="tw-mb-6">
{{ "userPermissionOverrideHelper" | i18n }} {{ "userPermissionOverrideHelper" | i18n }}
</div> </div>
<div *ngIf="!organization.flexibleCollections" class="tw-mb-6"> <div *ngIf="!organization.flexibleCollections" class="tw-mb-6">
@ -441,6 +449,7 @@
[selectorLabelText]="'selectCollections' | i18n" [selectorLabelText]="'selectCollections' | i18n"
[emptySelectionText]="'noCollectionsAdded' | i18n" [emptySelectionText]="'noCollectionsAdded' | i18n"
[flexibleCollectionsEnabled]="organization.flexibleCollections" [flexibleCollectionsEnabled]="organization.flexibleCollections"
[hideMultiSelect]="restrictedAccess$ | async"
></bit-access-selector ></bit-access-selector
></bit-tab> ></bit-tab>
</bit-tab-group> </bit-tab-group>

View File

@ -4,6 +4,7 @@ import { FormBuilder, Validators } from "@angular/forms";
import { import {
combineLatest, combineLatest,
firstValueFrom, firstValueFrom,
map,
Observable, Observable,
of, of,
shareReplay, shareReplay,
@ -20,7 +21,9 @@ import {
} from "@bitwarden/common/admin-console/enums"; } from "@bitwarden/common/admin-console/enums";
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ProductType } from "@bitwarden/common/enums"; import { ProductType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -99,6 +102,8 @@ export class MemberDialogComponent implements OnDestroy {
groups: [[] as AccessItemValue[]], groups: [[] as AccessItemValue[]],
}); });
protected restrictedAccess$: Observable<boolean>;
protected permissionsGroup = this.formBuilder.group({ protected permissionsGroup = this.formBuilder.group({
manageAssignedCollectionsGroup: this.formBuilder.group<Record<string, boolean>>({ manageAssignedCollectionsGroup: this.formBuilder.group<Record<string, boolean>>({
manageAssignedCollections: false, manageAssignedCollections: false,
@ -144,6 +149,7 @@ export class MemberDialogComponent implements OnDestroy {
private organizationUserService: OrganizationUserService, private organizationUserService: OrganizationUserService,
private dialogService: DialogService, private dialogService: DialogService,
private configService: ConfigServiceAbstraction, private configService: ConfigServiceAbstraction,
private accountService: AccountService,
organizationService: OrganizationService, organizationService: OrganizationService,
) { ) {
this.organization$ = organizationService this.organization$ = organizationService
@ -162,12 +168,42 @@ export class MemberDialogComponent implements OnDestroy {
), ),
); );
const userDetails$ = this.params.organizationUserId
? this.userService.get(this.params.organizationId, this.params.organizationUserId)
: of(null);
// The orgUser cannot manage their own Group assignments if collection access is restricted
// TODO: fix disabled state of access-selector rows so that any controls are hidden
this.restrictedAccess$ = combineLatest([
this.organization$,
userDetails$,
this.accountService.activeAccount$,
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
]).pipe(
map(
([organization, userDetails, activeAccount, flexibleCollectionsV1Enabled]) =>
// Feature flag conditionals
flexibleCollectionsV1Enabled &&
organization.flexibleCollections &&
// Business logic conditionals
userDetails.userId == activeAccount.id &&
!organization.allowAdminAccessToAllCollectionItems,
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
this.restrictedAccess$.pipe(takeUntil(this.destroy$)).subscribe((restrictedAccess) => {
if (restrictedAccess) {
this.formGroup.controls.groups.disable();
} else {
this.formGroup.controls.groups.enable();
}
});
combineLatest({ combineLatest({
organization: this.organization$, organization: this.organization$,
collections: this.collectionAdminService.getAll(this.params.organizationId), collections: this.collectionAdminService.getAll(this.params.organizationId),
userDetails: this.params.organizationUserId userDetails: userDetails$,
? this.userService.get(this.params.organizationId, this.params.organizationUserId)
: of(null),
groups: groups$, groups: groups$,
}) })
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
@ -369,7 +405,11 @@ export class MemberDialogComponent implements OnDestroy {
userView.collections = this.formGroup.value.access userView.collections = this.formGroup.value.access
.filter((v) => v.type === AccessItemType.Collection) .filter((v) => v.type === AccessItemType.Collection)
.map(convertToSelectionView); .map(convertToSelectionView);
userView.groups = this.formGroup.value.groups.map((m) => m.id);
userView.groups = (await firstValueFrom(this.restrictedAccess$))
? null
: this.formGroup.value.groups.map((m) => m.id);
userView.accessSecretsManager = this.formGroup.value.accessSecretsManager; userView.accessSecretsManager = this.formGroup.value.accessSecretsManager;
if (this.editMode) { if (this.editMode) {

View File

@ -1,6 +1,6 @@
<!-- Please remove this disable statement when editing this file! --> <!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable tailwindcss/no-custom-classname --> <!-- eslint-disable tailwindcss/no-custom-classname -->
<div class="tw-flex"> <div class="tw-flex" *ngIf="!hideMultiSelect">
<bit-form-field *ngIf="permissionMode == 'edit'" class="tw-mr-3 tw-shrink-0"> <bit-form-field *ngIf="permissionMode == 'edit'" class="tw-mr-3 tw-shrink-0">
<bit-label>{{ "permission" | i18n }}</bit-label> <bit-label>{{ "permission" | i18n }}</bit-label>
<!-- <!--

View File

@ -197,6 +197,11 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
this.permissionList = getPermissionList(value); this.permissionList = getPermissionList(value);
} }
/**
* Hide the multi-select so that new items cannot be added
*/
@Input() hideMultiSelect = false;
private _flexibleCollectionsEnabled: boolean; private _flexibleCollectionsEnabled: boolean;
constructor( constructor(

View File

@ -7605,5 +7605,11 @@
}, },
"providerPortal": { "providerPortal": {
"message": "Provider Portal" "message": "Provider Portal"
},
"restrictedGroupAccess": {
"message": "You cannot add yourself to groups."
},
"restrictedCollectionAccess": {
"message": "You cannot add yourself to collections."
} }
} }

View File

@ -8,7 +8,6 @@ import {
OrganizationUserInviteRequest, OrganizationUserInviteRequest,
OrganizationUserResetPasswordEnrollmentRequest, OrganizationUserResetPasswordEnrollmentRequest,
OrganizationUserResetPasswordRequest, OrganizationUserResetPasswordRequest,
OrganizationUserUpdateGroupsRequest,
OrganizationUserUpdateRequest, OrganizationUserUpdateRequest,
} from "./requests"; } from "./requests";
import { import {
@ -165,18 +164,6 @@ export abstract class OrganizationUserService {
request: OrganizationUserUpdateRequest, request: OrganizationUserUpdateRequest,
): Promise<void>; ): Promise<void>;
/**
* Update an organization user's groups
* @param organizationId - Identifier for the organization the user belongs to
* @param id - Organization user identifier
* @param groupIds - List of group ids to associate the user with
*/
abstract putOrganizationUserGroups(
organizationId: string,
id: string,
groupIds: OrganizationUserUpdateGroupsRequest,
): Promise<void>;
/** /**
* Update an organization user's reset password enrollment * Update an organization user's reset password enrollment
* @param organizationId - Identifier for the organization the user belongs to * @param organizationId - Identifier for the organization the user belongs to

View File

@ -6,4 +6,3 @@ export * from "./organization-user-invite.request";
export * from "./organization-user-reset-password.request"; export * from "./organization-user-reset-password.request";
export * from "./organization-user-reset-password-enrollment.request"; export * from "./organization-user-reset-password-enrollment.request";
export * from "./organization-user-update.request"; export * from "./organization-user-update.request";
export * from "./organization-user-update-groups.request";

View File

@ -1,3 +0,0 @@
export class OrganizationUserUpdateGroupsRequest {
groupIds: string[] = [];
}

View File

@ -9,7 +9,6 @@ import {
OrganizationUserInviteRequest, OrganizationUserInviteRequest,
OrganizationUserResetPasswordEnrollmentRequest, OrganizationUserResetPasswordEnrollmentRequest,
OrganizationUserResetPasswordRequest, OrganizationUserResetPasswordRequest,
OrganizationUserUpdateGroupsRequest,
OrganizationUserUpdateRequest, OrganizationUserUpdateRequest,
} from "../../abstractions/organization-user/requests"; } from "../../abstractions/organization-user/requests";
import { import {
@ -233,20 +232,6 @@ export class OrganizationUserServiceImplementation implements OrganizationUserSe
); );
} }
putOrganizationUserGroups(
organizationId: string,
id: string,
request: OrganizationUserUpdateGroupsRequest,
): Promise<void> {
return this.apiService.send(
"PUT",
"/organizations/" + organizationId + "/users/" + id + "/groups",
request,
true,
false,
);
}
putOrganizationUserResetPasswordEnrollment( putOrganizationUserResetPasswordEnrollment(
organizationId: string, organizationId: string,
userId: string, userId: string,