mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-17 01:31:25 +01:00
[PM-1337] Hide Organization options for users without master password (#6650)
* [PM-1337] Remove unused ModalService * [PM-1337] Use memberOrganization$ instead of deprecated isMember filter * [PM-1337] Move bitMenu into organization-options.component.html and update show/hide logic for various options * [PM-1337] Use observables for injected data in dynamic vault filter option Dynamic components do not currently support input data binding (available in Angular 16) so an observable must be passed into and subscribed by the dynamic component to receive updates. * [PM-1337] Cleanup organization-options.component.ts * [PM-1337] Use bitMenu directives instead of explicit TW classes * [PM-1337] Refactor app-link-sso into a directive to remove redundant template * [PM-1337] Fix failing tests
This commit is contained in:
parent
6c3cb841a2
commit
4446c09fd2
@ -1,9 +0,0 @@
|
||||
<a
|
||||
class="tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left !tw-text-main tw-no-underline hover:tw-bg-secondary-100 hover:tw-no-underline focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="submit(returnUri, true)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-link" aria-hidden="true"></i>
|
||||
{{ "linkSso" | i18n }}
|
||||
</a>
|
@ -1,4 +1,4 @@
|
||||
import { AfterContentInit, Component, Input } from "@angular/core";
|
||||
import { AfterContentInit, Directive, HostListener, Input } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { SsoComponent } from "@bitwarden/angular/auth/components/sso.component";
|
||||
@ -14,14 +14,19 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
@Component({
|
||||
selector: "app-link-sso",
|
||||
templateUrl: "link-sso.component.html",
|
||||
@Directive({
|
||||
selector: "[app-link-sso]",
|
||||
})
|
||||
export class LinkSsoComponent extends SsoComponent implements AfterContentInit {
|
||||
export class LinkSsoDirective extends SsoComponent implements AfterContentInit {
|
||||
@Input() organization: Organization;
|
||||
returnUri = "/settings/organizations";
|
||||
|
||||
@HostListener("click", ["$event"])
|
||||
async onClick($event: MouseEvent) {
|
||||
$event.preventDefault();
|
||||
await this.submit(this.returnUri, true);
|
||||
}
|
||||
|
||||
constructor(
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
@ -1,54 +1,60 @@
|
||||
<ng-container *ngIf="!loaded">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted tw-m-2"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<div
|
||||
*ngIf="loaded"
|
||||
class="tw-flex tw-min-w-[200px] tw-max-w-[300px] tw-flex-col"
|
||||
[appApiAction]="actionPromise"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="allowEnrollmentChanges(organization) && !organization.resetPasswordEnrolled"
|
||||
class="tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left !tw-text-main hover:tw-bg-secondary-100 focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
|
||||
(click)="toggleResetPasswordEnrollment(organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>
|
||||
{{ "enrollAccountRecovery" | i18n }}
|
||||
<ng-container *ngIf="!hideMenu">
|
||||
<button type="button" [bitMenuTriggerFor]="optionsMenu" class="filter-options-icon">
|
||||
<i class="bwi bwi-ellipsis-v" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="allowEnrollmentChanges(organization) && organization.resetPasswordEnrolled"
|
||||
class="tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left !tw-text-main hover:tw-bg-secondary-100 focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
|
||||
(click)="toggleResetPasswordEnrollment(organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||
{{ "withdrawAccountRecovery" | i18n }}
|
||||
</button>
|
||||
<ng-container *ngIf="organization.useSso && organization.identifier">
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="organization.ssoBound; else linkSso"
|
||||
class="tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left !tw-text-main hover:tw-bg-secondary-100 focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
|
||||
(click)="unlinkSso(organization)"
|
||||
<bit-menu class="filter-organization-options" #optionsMenu>
|
||||
<ng-container *ngIf="!loaded">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-m-2 tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<div
|
||||
*ngIf="loaded"
|
||||
class="tw-flex tw-min-w-[200px] tw-max-w-[300px] tw-flex-col"
|
||||
[appApiAction]="actionPromise"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-chain-broken" aria-hidden="true"></i>
|
||||
{{ "unlinkSso" | i18n }}
|
||||
</button>
|
||||
<ng-template #linkSso>
|
||||
<app-link-sso [organization]="organization"> </app-link-sso>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<button
|
||||
type="button"
|
||||
class="text-danger tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left hover:tw-bg-secondary-100 focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
|
||||
(click)="leave(organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-sign-out" aria-hidden="true"></i>
|
||||
{{ "leave" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="allowEnrollmentChanges(organization) && !organization.resetPasswordEnrolled"
|
||||
bitMenuItem
|
||||
(click)="toggleResetPasswordEnrollment(organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>
|
||||
{{ "enrollAccountRecovery" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="allowEnrollmentChanges(organization) && organization.resetPasswordEnrolled"
|
||||
bitMenuItem
|
||||
(click)="toggleResetPasswordEnrollment(organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||
{{ "withdrawAccountRecovery" | i18n }}
|
||||
</button>
|
||||
<ng-container *ngIf="showSsoOptions(organization)">
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="organization.ssoBound; else linkSso"
|
||||
bitMenuItem
|
||||
(click)="unlinkSso(organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-chain-broken" aria-hidden="true"></i>
|
||||
{{ "unlinkSso" | i18n }}
|
||||
</button>
|
||||
<ng-template #linkSso>
|
||||
<a href="#" bitMenuItem app-link-sso [organization]="organization">
|
||||
<i class="bwi bwi-fw bwi-link" aria-hidden="true"></i>
|
||||
{{ "linkSso" | i18n }}
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<button *ngIf="showLeaveOrgOption" type="button" bitMenuItem (click)="leave(organization)">
|
||||
<i class="bwi bwi-fw bwi-sign-out tw-text-danger" aria-hidden="true"></i>
|
||||
<span class="tw-text-danger">{{ "leave" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</bit-menu>
|
||||
</ng-container>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { map, Subject, takeUntil } from "rxjs";
|
||||
import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
@ -13,6 +12,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@ -25,34 +25,58 @@ import { OrganizationFilter } from "../shared/models/vault-filter.type";
|
||||
templateUrl: "organization-options.component.html",
|
||||
})
|
||||
export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
||||
actionPromise: Promise<void | boolean>;
|
||||
policies: Policy[];
|
||||
loaded = false;
|
||||
protected actionPromise: Promise<void | boolean>;
|
||||
protected resetPasswordPolicy?: Policy | undefined;
|
||||
protected loaded = false;
|
||||
protected hideMenu = false;
|
||||
protected showLeaveOrgOption = false;
|
||||
protected organization: OrganizationFilter;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
@Inject(OptionsInput) protected organization: OrganizationFilter,
|
||||
@Inject(OptionsInput) protected organization$: Observable<OrganizationFilter>,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private apiService: ApiService,
|
||||
private syncService: SyncService,
|
||||
private policyService: PolicyService,
|
||||
private modalService: ModalService,
|
||||
private logService: LogService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private dialogService: DialogService
|
||||
private dialogService: DialogService,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.policyService.policies$
|
||||
.pipe(
|
||||
map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe((policies) => {
|
||||
this.policies = policies;
|
||||
const resetPasswordPolicies$ = this.policyService.policies$.pipe(
|
||||
map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword))
|
||||
);
|
||||
|
||||
combineLatest([
|
||||
this.organization$,
|
||||
resetPasswordPolicies$,
|
||||
this.stateService.getAccountDecryptionOptions(),
|
||||
])
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(([organization, resetPasswordPolicies, decryptionOptions]) => {
|
||||
this.organization = organization;
|
||||
this.resetPasswordPolicy = resetPasswordPolicies.find(
|
||||
(p) => p.organizationId === organization.id
|
||||
);
|
||||
|
||||
// A user can leave an organization if they are NOT using TDE and Key Connector, or they have a master password.
|
||||
this.showLeaveOrgOption =
|
||||
(decryptionOptions.trustedDeviceOption == undefined &&
|
||||
decryptionOptions.keyConnectorOption == undefined) ||
|
||||
decryptionOptions.hasMasterPassword;
|
||||
|
||||
// Hide the 3 dot menu if the user has no available actions
|
||||
this.hideMenu =
|
||||
!this.showLeaveOrgOption &&
|
||||
!this.showSsoOptions(this.organization) &&
|
||||
!this.allowEnrollmentChanges(this.organization);
|
||||
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
@ -64,21 +88,16 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
||||
|
||||
allowEnrollmentChanges(org: OrganizationFilter): boolean {
|
||||
if (org.usePolicies && org.useResetPassword && org.hasPublicAndPrivateKeys) {
|
||||
const policy = this.policies.find((p) => p.organizationId === org.id);
|
||||
if (policy != null && policy.enabled) {
|
||||
return org.resetPasswordEnrolled && policy.data.autoEnrollEnabled ? false : true;
|
||||
if (this.resetPasswordPolicy != undefined && this.resetPasswordPolicy.enabled) {
|
||||
return !(org.resetPasswordEnrolled && this.resetPasswordPolicy.data.autoEnrollEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
showEnrolledStatus(org: Organization): boolean {
|
||||
return (
|
||||
org.useResetPassword &&
|
||||
org.resetPasswordEnrolled &&
|
||||
this.policies.some((p) => p.organizationId === org.id && p.enabled)
|
||||
);
|
||||
showSsoOptions(org: OrganizationFilter) {
|
||||
return org.useSso && org.identifier;
|
||||
}
|
||||
|
||||
async unlinkSso(org: Organization) {
|
||||
@ -143,7 +162,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
||||
null,
|
||||
this.i18nService.t("withdrawPasswordResetSuccess")
|
||||
);
|
||||
this.syncService.fullSync(true);
|
||||
await this.syncService.fullSync(true);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ describe("vault filter service", () => {
|
||||
organizations = new ReplaySubject<Organization[]>(1);
|
||||
folderViews = new ReplaySubject<FolderView[]>(1);
|
||||
|
||||
organizationService.organizations$ = organizations;
|
||||
organizationService.memberOrganizations$ = organizations;
|
||||
folderService.folderViews$ = folderViews;
|
||||
|
||||
vaultFilterService = new VaultFilterService(
|
||||
|
@ -11,10 +11,7 @@ import {
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
import {
|
||||
isMember,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
@ -48,7 +45,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
);
|
||||
|
||||
organizationTree$: Observable<TreeNode<OrganizationFilter>> =
|
||||
this.organizationService.organizations$.pipe(
|
||||
this.organizationService.memberOrganizations$.pipe(
|
||||
switchMap((orgs) => this.buildOrganizationTree(orgs))
|
||||
);
|
||||
|
||||
@ -139,7 +136,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
}
|
||||
if (orgs) {
|
||||
const orgNodes: TreeNode<OrganizationFilter>[] = [];
|
||||
orgs.filter(isMember).forEach((org) => {
|
||||
orgs.forEach((org) => {
|
||||
const orgCopy = org as OrganizationFilter;
|
||||
orgCopy.icon = "bwi-business";
|
||||
const node = new TreeNode<OrganizationFilter>(orgCopy, headNode, orgCopy.name);
|
||||
|
@ -98,16 +98,11 @@
|
||||
class="org-options bwi bwi-fw bwi-exclamation-triangle text-danger"
|
||||
[attr.aria-label]="'organizationIsDisabled' | i18n"
|
||||
appA11yTitle="{{ 'organizationIsDisabled' | i18n }}"
|
||||
></i
|
||||
><ng-container *ngIf="optionsInfo && !f.node.hideOptions"
|
||||
><button type="button" [bitMenuTriggerFor]="optionsMenu" class="filter-options-icon">
|
||||
<i class="bwi bwi-ellipsis-v" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu class="filter-organization-options" #optionsMenu>
|
||||
<ng-container
|
||||
*ngComponentOutlet="optionsInfo.component; injector: createInjector(f.node)"
|
||||
></ng-container>
|
||||
</bit-menu>
|
||||
></i>
|
||||
<ng-container *ngIf="optionsInfo && !f.node.hideOptions">
|
||||
<ng-container
|
||||
*ngComponentOutlet="optionsInfo.component; injector: createInjector(f.node)"
|
||||
></ng-container>
|
||||
</ng-container>
|
||||
</span>
|
||||
</span>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Component, InjectionToken, Injector, Input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { Observable, Subject, takeUntil } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ITreeNodeObject, TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||
@ -120,9 +121,15 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
||||
// here we are creating a new injector for each filter that has options
|
||||
createInjector(data: VaultFilterType) {
|
||||
let inject = this.injectors.get(data.id);
|
||||
|
||||
if (!inject) {
|
||||
// Pass an observable to the component in order to update the component when the data changes
|
||||
// as data binding does not work with dynamic components in Angular 15 (inputs are supported starting Angular 16)
|
||||
const data$ = this.section.data$.pipe(
|
||||
map((sectionNode) => sectionNode?.children?.find((node) => node.node.id === data.id)?.node)
|
||||
);
|
||||
inject = Injector.create({
|
||||
providers: [{ provide: OptionsInput, useValue: data }],
|
||||
providers: [{ provide: OptionsInput, useValue: data$ }],
|
||||
parent: this.injector,
|
||||
});
|
||||
this.injectors.set(data.id, inject);
|
||||
@ -130,4 +137,4 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
||||
return inject;
|
||||
}
|
||||
}
|
||||
export const OptionsInput = new InjectionToken<VaultFilterType>("OptionsInput");
|
||||
export const OptionsInput = new InjectionToken<Observable<VaultFilterType>>("OptionsInput");
|
||||
|
@ -2,7 +2,7 @@ import { NgModule } from "@angular/core";
|
||||
|
||||
import { VaultFilterSharedModule } from "../../individual-vault/vault-filter/shared/vault-filter-shared.module";
|
||||
|
||||
import { LinkSsoComponent } from "./components/link-sso.component";
|
||||
import { LinkSsoDirective } from "./components/link-sso.directive";
|
||||
import { OrganizationOptionsComponent } from "./components/organization-options.component";
|
||||
import { VaultFilterComponent } from "./components/vault-filter.component";
|
||||
import { VaultFilterService as VaultFilterServiceAbstraction } from "./services/abstractions/vault-filter.service";
|
||||
@ -10,7 +10,7 @@ import { VaultFilterService } from "./services/vault-filter.service";
|
||||
|
||||
@NgModule({
|
||||
imports: [VaultFilterSharedModule],
|
||||
declarations: [VaultFilterComponent, OrganizationOptionsComponent, LinkSsoComponent],
|
||||
declarations: [VaultFilterComponent, OrganizationOptionsComponent, LinkSsoDirective],
|
||||
exports: [VaultFilterComponent],
|
||||
providers: [
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user