mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-20050] Readonly members page for users with Account Recovery permission only (#14647)
* Add SeparateCustomRolePermissions feature flag * Allow 'Manage Account Recovery' to be configured separately from 'Manage Users' * Add showUserManagementControls$ observable to show/hide user management controls based on user permissions * Update user management controls visibility to be dependant on user permissions.
This commit is contained in:
@@ -5,7 +5,14 @@
|
||||
[placeholder]="'searchMembers' | i18n"
|
||||
></bit-search>
|
||||
|
||||
<button type="button" bitButton buttonType="primary" (click)="invite()" [disabled]="!firstLoaded">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="invite()"
|
||||
[disabled]="!firstLoaded"
|
||||
*ngIf="showUserManagementControls$ | async"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "inviteMember" | i18n }}
|
||||
</button>
|
||||
@@ -16,6 +23,7 @@
|
||||
[selected]="status"
|
||||
(selectedChange)="statusToggle.next($event)"
|
||||
[attr.aria-label]="'memberStatusFilter' | i18n"
|
||||
*ngIf="showUserManagementControls$ | async"
|
||||
>
|
||||
<bit-toggle [value]="null">
|
||||
{{ "all" | i18n }}
|
||||
@@ -71,7 +79,7 @@
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell class="tw-w-20">
|
||||
<th bitCell class="tw-w-20" *ngIf="showUserManagementControls$ | async">
|
||||
<input
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
@@ -94,6 +102,7 @@
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
size="small"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
*ngIf="showUserManagementControls$ | async"
|
||||
></button>
|
||||
|
||||
<bit-menu #headerMenu>
|
||||
@@ -174,74 +183,143 @@
|
||||
alignContent="middle"
|
||||
[ngClass]="rowHeightClass"
|
||||
>
|
||||
<td bitCell (click)="dataSource.checkUser(u)">
|
||||
<td
|
||||
bitCell
|
||||
(click)="dataSource.checkUser(u)"
|
||||
*ngIf="showUserManagementControls$ | async"
|
||||
>
|
||||
<input type="checkbox" bitCheckbox [(ngModel)]="$any(u).checked" />
|
||||
</td>
|
||||
<td bitCell (click)="edit(u)" class="tw-cursor-pointer">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
size="small"
|
||||
[text]="u | userName"
|
||||
[id]="u.userId"
|
||||
[color]="u.avatarColor"
|
||||
class="tw-mr-3"
|
||||
></bit-avatar>
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<div class="tw-flex tw-flex-row tw-gap-2">
|
||||
<button type="button" bitLink>
|
||||
{{ u.name ?? u.email }}
|
||||
</button>
|
||||
<span
|
||||
bitBadge
|
||||
class="tw-text-xs"
|
||||
variant="secondary"
|
||||
*ngIf="u.status === userStatusType.Invited"
|
||||
>
|
||||
{{ "invited" | i18n }}
|
||||
</span>
|
||||
<span
|
||||
bitBadge
|
||||
class="tw-text-xs"
|
||||
variant="warning"
|
||||
*ngIf="u.status === userStatusType.Accepted"
|
||||
>
|
||||
{{ "needsConfirmation" | i18n }}
|
||||
</span>
|
||||
<span
|
||||
bitBadge
|
||||
class="tw-text-xs"
|
||||
variant="secondary"
|
||||
*ngIf="u.status === userStatusType.Revoked"
|
||||
>
|
||||
{{ "revoked" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
|
||||
{{ u.email }}
|
||||
<ng-container *ngIf="showUserManagementControls$ | async; else readOnlyUserInfo">
|
||||
<td bitCell (click)="edit(u)" class="tw-cursor-pointer">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
size="small"
|
||||
[text]="u | userName"
|
||||
[id]="u.userId"
|
||||
[color]="u.avatarColor"
|
||||
class="tw-mr-3"
|
||||
></bit-avatar>
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<div class="tw-flex tw-flex-row tw-gap-2">
|
||||
<button type="button" bitLink>
|
||||
{{ u.name ?? u.email }}
|
||||
</button>
|
||||
<span
|
||||
bitBadge
|
||||
class="tw-text-xs"
|
||||
variant="secondary"
|
||||
*ngIf="u.status === userStatusType.Invited"
|
||||
>
|
||||
{{ "invited" | i18n }}
|
||||
</span>
|
||||
<span
|
||||
bitBadge
|
||||
class="tw-text-xs"
|
||||
variant="warning"
|
||||
*ngIf="u.status === userStatusType.Accepted"
|
||||
>
|
||||
{{ "needsConfirmation" | i18n }}
|
||||
</span>
|
||||
<span
|
||||
bitBadge
|
||||
class="tw-text-xs"
|
||||
variant="secondary"
|
||||
*ngIf="u.status === userStatusType.Revoked"
|
||||
>
|
||||
{{ "revoked" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
|
||||
{{ u.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-template #readOnlyUserInfo>
|
||||
<td bitCell>
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
size="small"
|
||||
[text]="u | userName"
|
||||
[id]="u.userId"
|
||||
[color]="u.avatarColor"
|
||||
class="tw-mr-3"
|
||||
></bit-avatar>
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<div class="tw-flex tw-flex-row tw-gap-2">
|
||||
<span>{{ u.name ?? u.email }}</span>
|
||||
<span
|
||||
bitBadge
|
||||
class="tw-text-xs"
|
||||
variant="secondary"
|
||||
*ngIf="u.status === userStatusType.Invited"
|
||||
>
|
||||
{{ "invited" | i18n }}
|
||||
</span>
|
||||
<span
|
||||
bitBadge
|
||||
class="tw-text-xs"
|
||||
variant="warning"
|
||||
*ngIf="u.status === userStatusType.Accepted"
|
||||
>
|
||||
{{ "needsConfirmation" | i18n }}
|
||||
</span>
|
||||
<span
|
||||
bitBadge
|
||||
class="tw-text-xs"
|
||||
variant="secondary"
|
||||
*ngIf="u.status === userStatusType.Revoked"
|
||||
>
|
||||
{{ "revoked" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
|
||||
{{ u.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-template>
|
||||
|
||||
<td
|
||||
bitCell
|
||||
(click)="edit(u, organization.useGroups ? memberTab.Groups : memberTab.Collections)"
|
||||
class="tw-cursor-pointer"
|
||||
>
|
||||
<bit-badge-list
|
||||
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
|
||||
[maxItems]="3"
|
||||
variant="secondary"
|
||||
></bit-badge-list>
|
||||
</td>
|
||||
<ng-container *ngIf="showUserManagementControls$ | async; else readOnlyGroupsCell">
|
||||
<td
|
||||
bitCell
|
||||
(click)="edit(u, organization.useGroups ? memberTab.Groups : memberTab.Collections)"
|
||||
class="tw-cursor-pointer"
|
||||
>
|
||||
<bit-badge-list
|
||||
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
|
||||
[maxItems]="3"
|
||||
variant="secondary"
|
||||
></bit-badge-list>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-template #readOnlyGroupsCell>
|
||||
<td bitCell>
|
||||
<bit-badge-list
|
||||
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
|
||||
[maxItems]="3"
|
||||
variant="secondary"
|
||||
></bit-badge-list>
|
||||
</td>
|
||||
</ng-template>
|
||||
|
||||
<td
|
||||
bitCell
|
||||
(click)="edit(u, memberTab.Role)"
|
||||
class="tw-cursor-pointer tw-text-sm tw-text-muted"
|
||||
>
|
||||
{{ u.type | userType }}
|
||||
</td>
|
||||
<ng-container *ngIf="showUserManagementControls$ | async; else readOnlyRoleCell">
|
||||
<td
|
||||
bitCell
|
||||
(click)="edit(u, memberTab.Role)"
|
||||
class="tw-cursor-pointer tw-text-sm tw-text-muted"
|
||||
>
|
||||
{{ u.type | userType }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-template #readOnlyRoleCell>
|
||||
<td bitCell class="tw-text-sm tw-text-muted">
|
||||
{{ u.type | userType }}
|
||||
</td>
|
||||
</ng-template>
|
||||
|
||||
<td bitCell class="tw-text-muted">
|
||||
<ng-container *ngIf="u.twoFactorEnabled">
|
||||
@@ -271,54 +349,58 @@
|
||||
></button>
|
||||
|
||||
<bit-menu #rowMenu>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="reinvite(u)"
|
||||
*ngIf="u.status === userStatusType.Invited"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="confirm(u)"
|
||||
*ngIf="u.status === userStatusType.Accepted"
|
||||
>
|
||||
<span class="tw-text-success">
|
||||
<i aria-hidden="true" class="bwi bwi-check"></i> {{ "confirm" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
<bit-menu-divider
|
||||
*ngIf="
|
||||
u.status === userStatusType.Accepted || u.status === userStatusType.Invited
|
||||
"
|
||||
></bit-menu-divider>
|
||||
<button type="button" bitMenuItem (click)="edit(u, memberTab.Role)">
|
||||
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "memberRole" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="edit(u, memberTab.Groups)"
|
||||
*ngIf="organization.useGroups"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="edit(u, memberTab.Collections)">
|
||||
<i aria-hidden="true" class="bwi bwi-collection-shared"></i>
|
||||
{{ "collections" | i18n }}
|
||||
</button>
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="openEventsDialog(u)"
|
||||
*ngIf="organization.useEvents && u.status === userStatusType.Confirmed"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-file-text"></i> {{ "eventLogs" | i18n }}
|
||||
</button>
|
||||
<ng-container *ngIf="showUserManagementControls$ | async">
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="reinvite(u)"
|
||||
*ngIf="u.status === userStatusType.Invited"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="confirm(u)"
|
||||
*ngIf="u.status === userStatusType.Accepted"
|
||||
>
|
||||
<span class="tw-text-success">
|
||||
<i aria-hidden="true" class="bwi bwi-check"></i> {{ "confirm" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
<bit-menu-divider
|
||||
*ngIf="
|
||||
u.status === userStatusType.Accepted || u.status === userStatusType.Invited
|
||||
"
|
||||
></bit-menu-divider>
|
||||
<button type="button" bitMenuItem (click)="edit(u, memberTab.Role)">
|
||||
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "memberRole" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="edit(u, memberTab.Groups)"
|
||||
*ngIf="organization.useGroups"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="edit(u, memberTab.Collections)">
|
||||
<i aria-hidden="true" class="bwi bwi-collection-shared"></i>
|
||||
{{ "collections" | i18n }}
|
||||
</button>
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="openEventsDialog(u)"
|
||||
*ngIf="organization.useEvents && u.status === userStatusType.Confirmed"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-file-text"></i> {{ "eventLogs" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<!-- Account recovery is available to all users with appropriate permissions -->
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
@@ -327,45 +409,48 @@
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-key"></i> {{ "recoverAccount" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="restore(u)"
|
||||
*ngIf="u.status === userStatusType.Revoked"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
|
||||
{{ "restoreAccess" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="revoke(u)"
|
||||
*ngIf="u.status !== userStatusType.Revoked"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
|
||||
{{ "revokeAccess" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="!u.managedByOrganization"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="remove(u)"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="u.managedByOrganization"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="deleteUser(u)"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-trash" aria-hidden="true"></i>
|
||||
{{ "delete" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ng-container *ngIf="showUserManagementControls$ | async">
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="restore(u)"
|
||||
*ngIf="u.status === userStatusType.Revoked"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
|
||||
{{ "restoreAccess" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="revoke(u)"
|
||||
*ngIf="u.status !== userStatusType.Revoked"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
|
||||
{{ "revokeAccess" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="!u.managedByOrganization"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="remove(u)"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="u.managedByOrganization"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="deleteUser(u)"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-trash" aria-hidden="true"></i>
|
||||
{{ "delete" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -46,7 +46,9 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
@@ -102,6 +104,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
orgIsOnSecretsManagerStandalone = false;
|
||||
|
||||
protected canUseSecretsManager$: Observable<boolean>;
|
||||
protected showUserManagementControls$: Observable<boolean>;
|
||||
|
||||
// Fixed sizes used for cdkVirtualScroll
|
||||
protected rowHeight = 69;
|
||||
@@ -135,6 +138,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
private collectionService: CollectionService,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
@@ -229,6 +233,17 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// Setup feature flag-dependent observables
|
||||
const separateCustomRolePermissionsEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.SeparateCustomRolePermissions,
|
||||
);
|
||||
this.showUserManagementControls$ = separateCustomRolePermissionsEnabled$.pipe(
|
||||
map(
|
||||
(separateCustomRolePermissionsEnabled) =>
|
||||
!separateCustomRolePermissionsEnabled || this.organization.canManageUsers,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async getUsers(): Promise<OrganizationUserView[]> {
|
||||
|
||||
Reference in New Issue
Block a user