From 4446c09fd2d61c421b46fba9d3a825acb6e6cdce Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 9 Nov 2023 10:12:00 -0800 Subject: [PATCH] [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 --- .../components/link-sso.component.html | 9 -- ...sso.component.ts => link-sso.directive.ts} | 15 ++- .../organization-options.component.html | 110 +++++++++--------- .../organization-options.component.ts | 69 +++++++---- .../services/vault-filter.service.spec.ts | 2 +- .../services/vault-filter.service.ts | 9 +- .../vault-filter-section.component.html | 15 +-- .../vault-filter-section.component.ts | 13 ++- .../vault-filter/vault-filter.module.ts | 4 +- 9 files changed, 133 insertions(+), 113 deletions(-) delete mode 100644 apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.html rename apps/web/src/app/vault/individual-vault/vault-filter/components/{link-sso.component.ts => link-sso.directive.ts} (86%) diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.html deleted file mode 100644 index 788b7d4ab98..00000000000 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {{ "linkSso" | i18n }} - diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.directive.ts similarity index 86% rename from apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.ts rename to apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.directive.ts index 026e1f883a9..e7341c5c333 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.directive.ts @@ -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, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html index 15f3f80b3f5..f4fb2cc040a 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html @@ -1,54 +1,60 @@ - - - {{ "loading" | i18n }} - -
- - - - - - - - - -
+ + + + + + + + {{ "linkSso" | i18n }} + + + + + + + diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index 35c51643889..f8c60b26232 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -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; - policies: Policy[]; - loaded = false; + protected actionPromise: Promise; + protected resetPasswordPolicy?: Policy | undefined; + protected loaded = false; + protected hideMenu = false; + protected showLeaveOrgOption = false; + protected organization: OrganizationFilter; private destroy$ = new Subject(); constructor( - @Inject(OptionsInput) protected organization: OrganizationFilter, + @Inject(OptionsInput) protected organization$: Observable, 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); } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts index 88f004262e5..23ad5e1a4ae 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts @@ -39,7 +39,7 @@ describe("vault filter service", () => { organizations = new ReplaySubject(1); folderViews = new ReplaySubject(1); - organizationService.organizations$ = organizations; + organizationService.memberOrganizations$ = organizations; folderService.folderViews$ = folderViews; vaultFilterService = new VaultFilterService( diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index f7663ebc42c..db0dffbdd71 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -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> = - 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[] = []; - orgs.filter(isMember).forEach((org) => { + orgs.forEach((org) => { const orgCopy = org as OrganizationFilter; orgCopy.icon = "bwi-business"; const node = new TreeNode(orgCopy, headNode, orgCopy.name); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html index bcd151193a5..aac08395205 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html @@ -98,16 +98,11 @@ class="org-options bwi bwi-fw bwi-exclamation-triangle text-danger" [attr.aria-label]="'organizationIsDisabled' | i18n" appA11yTitle="{{ 'organizationIsDisabled' | i18n }}" - > - - - + > + + diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts index a15521dc228..21e6ef727da 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts @@ -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("OptionsInput"); +export const OptionsInput = new InjectionToken>("OptionsInput"); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts b/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts index b6edd3e9838..2acab17e383 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts @@ -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: [ {