diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2f402b15dd5..de28b210887 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,7 +8,8 @@ apps/desktop/desktop_native @bitwarden/team-platform-dev apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev -## No ownership for Cargo.toml to allow dependency updates +## No ownership fo Cargo.lock and Cargo.toml to allow dependency updates +apps/desktop/desktop_native/Cargo.lock apps/desktop/desktop_native/Cargo.toml ## Auth team files ## diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 87b94650b51..b0f3fb5d19e 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2,6 +2,9 @@ "appName": { "message": "Bitwarden" }, + "appLogoLabel": { + "message": "Bitwarden logo" + }, "extName": { "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html index 54cb5203a87..bd2886dacf0 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html @@ -5,7 +5,12 @@ [showBackButton]="showBackButton" [pageTitle]="''" > - + diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index 51dbb6503d7..d6cccf31bb4 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -12,6 +12,7 @@ import { } from "@bitwarden/auth/angular"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Icon, IconModule, Translation } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; @@ -36,6 +37,7 @@ export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData { AnonLayoutComponent, CommonModule, CurrentAccountComponent, + I18nPipe, IconModule, PopOutComponent, PopupPageComponent, diff --git a/apps/browser/src/auth/popup/remove-password.component.html b/apps/browser/src/key-management/key-connector/remove-password.component.html similarity index 100% rename from apps/browser/src/auth/popup/remove-password.component.html rename to apps/browser/src/key-management/key-connector/remove-password.component.html diff --git a/apps/browser/src/auth/popup/remove-password.component.ts b/apps/browser/src/key-management/key-connector/remove-password.component.ts similarity index 80% rename from apps/browser/src/auth/popup/remove-password.component.ts rename to apps/browser/src/key-management/key-connector/remove-password.component.ts index 5272a3082a2..3ca9d3a5669 100644 --- a/apps/browser/src/auth/popup/remove-password.component.ts +++ b/apps/browser/src/key-management/key-connector/remove-password.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; -import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component"; +import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui"; @Component({ selector: "app-remove-password", diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 21ac4c19700..45955506b91 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -55,7 +55,6 @@ import { ExtensionAnonLayoutWrapperComponent, ExtensionAnonLayoutWrapperData, } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; -import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; @@ -65,6 +64,7 @@ import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-doma import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service"; import { CredentialGeneratorHistoryComponent } from "../tools/popup/generator/credential-generator-history.component"; diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 0d392afa63b..8bea41da4d6 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -25,13 +25,13 @@ import { import { AccountComponent } from "../auth/popup/account-switching/account.component"; import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component"; import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; -import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { PopOutComponent } from "../platform/popup/components/pop-out.component"; import { HeaderComponent } from "../platform/popup/header.component"; import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component"; diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index 6d524759dd6..d10c3f38ebd 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -17,7 +17,7 @@ import { MasterKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { KeyService } from "@bitwarden/key-management"; -import { ConvertToKeyConnectorCommand } from "../../commands/convert-to-key-connector.command"; +import { ConvertToKeyConnectorCommand } from "../../key-management/convert-to-key-connector.command"; import { Response } from "../../models/response"; import { MessageResponse } from "../../models/response/message.response"; import { CliUtils } from "../../utils"; diff --git a/apps/cli/src/commands/convert-to-key-connector.command.ts b/apps/cli/src/key-management/convert-to-key-connector.command.ts similarity index 100% rename from apps/cli/src/commands/convert-to-key-connector.command.ts rename to apps/cli/src/key-management/convert-to-key-connector.command.ts diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index cd5064a87e4..0c6bc730c2c 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -50,9 +50,9 @@ import { import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; -import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component"; diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index fdc25ed642e..b892324a979 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -13,11 +13,11 @@ import { DecryptionFailureDialogComponent } from "@bitwarden/vault"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { DeleteAccountComponent } from "../auth/delete-account.component"; import { LoginModule } from "../auth/login/login.module"; -import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { SshAgentService } from "../autofill/services/ssh-agent.service"; import { PremiumComponent } from "../billing/app/accounts/premium.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { AddEditCustomFieldsComponent } from "../vault/app/vault/add-edit-custom-fields.component"; import { AddEditComponent } from "../vault/app/vault/add-edit.component"; import { AttachmentsComponent } from "../vault/app/vault/attachments.component"; diff --git a/apps/desktop/src/auth/remove-password.component.html b/apps/desktop/src/key-management/key-connector/remove-password.component.html similarity index 100% rename from apps/desktop/src/auth/remove-password.component.html rename to apps/desktop/src/key-management/key-connector/remove-password.component.html diff --git a/apps/desktop/src/auth/remove-password.component.ts b/apps/desktop/src/key-management/key-connector/remove-password.component.ts similarity index 80% rename from apps/desktop/src/auth/remove-password.component.ts rename to apps/desktop/src/key-management/key-connector/remove-password.component.ts index 5272a3082a2..3ca9d3a5669 100644 --- a/apps/desktop/src/auth/remove-password.component.ts +++ b/apps/desktop/src/key-management/key-connector/remove-password.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; -import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component"; +import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui"; @Component({ selector: "app-remove-password", diff --git a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts index 9afd34ca149..d628e23063f 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts @@ -100,20 +100,44 @@ describe("Organization Permissions Guard", () => { it("permits navigation if the user has permissions", async () => { const permissionsCallback = jest.fn(); - permissionsCallback.mockImplementation((_org) => true); + permissionsCallback.mockReturnValue(true); const actual = await TestBed.runInInjectionContext( async () => await organizationPermissionsGuard(permissionsCallback)(route, state), ); - expect(permissionsCallback).toHaveBeenCalledWith(orgFactory({ id: targetOrgId })); + expect(permissionsCallback).toHaveBeenCalledTimes(1); + expect(actual).toBe(true); + }); + + it("handles a Promise returned from the callback", async () => { + const permissionsCallback = jest.fn(); + permissionsCallback.mockReturnValue(Promise.resolve(true)); + + const actual = await TestBed.runInInjectionContext(() => + organizationPermissionsGuard(permissionsCallback)(route, state), + ); + + expect(permissionsCallback).toHaveBeenCalledTimes(1); + expect(actual).toBe(true); + }); + + it("handles an Observable returned from the callback", async () => { + const permissionsCallback = jest.fn(); + permissionsCallback.mockReturnValue(of(true)); + + const actual = await TestBed.runInInjectionContext(() => + organizationPermissionsGuard(permissionsCallback)(route, state), + ); + + expect(permissionsCallback).toHaveBeenCalledTimes(1); expect(actual).toBe(true); }); describe("if the user does not have permissions", () => { it("and there is no Item ID, block navigation", async () => { const permissionsCallback = jest.fn(); - permissionsCallback.mockImplementation((_org) => false); + permissionsCallback.mockReturnValue(false); state = mock({ root: mock({ diff --git a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts index d399f9c9c05..6c9090a27b4 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts @@ -1,13 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { inject } from "@angular/core"; +import { EnvironmentInjector, inject, runInInjectionContext } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot, } from "@angular/router"; -import { firstValueFrom, switchMap } from "rxjs"; +import { firstValueFrom, isObservable, Observable, switchMap } from "rxjs"; import { canAccessOrgAdmin, @@ -42,7 +42,9 @@ import { ToastService } from "@bitwarden/components"; * proceeds as expected. */ export function organizationPermissionsGuard( - permissionsCallback?: (organization: Organization) => boolean, + permissionsCallback?: ( + organization: Organization, + ) => boolean | Promise | Observable, ): CanActivateFn { return async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { const router = inject(Router); @@ -51,6 +53,7 @@ export function organizationPermissionsGuard( const i18nService = inject(I18nService); const syncService = inject(SyncService); const accountService = inject(AccountService); + const environmentInjector = inject(EnvironmentInjector); // TODO: We need to fix issue once and for all. if ((await syncService.getLastSync()) == null) { @@ -78,7 +81,22 @@ export function organizationPermissionsGuard( return router.createUrlTree(["/"]); } - const hasPermissions = permissionsCallback == null || permissionsCallback(org); + if (permissionsCallback == null) { + // No additional permission checks required, allow navigation + return true; + } + + const callbackResult = runInInjectionContext(environmentInjector, () => + permissionsCallback(org), + ); + + const hasPermissions = isObservable(callbackResult) + ? await firstValueFrom(callbackResult) // handles observables + : await Promise.resolve(callbackResult); // handles promises and boolean values + + if (hasPermissions !== true && hasPermissions !== false) { + throw new Error("Permission callback did not resolve to a boolean."); + } if (!hasPermissions) { // Handle linkable ciphers for organizations the user only has view access to diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index e50c55e83d2..fec790dabcb 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -97,7 +97,7 @@ ; protected isBreadcrumbEventLogsEnabled$: Observable; protected showSponsoredFamiliesDropdown$: Observable; + protected canShowPoliciesTab$: Observable; constructor( private route: ActivatedRoute, @@ -79,6 +81,7 @@ export class OrganizationLayoutComponent implements OnInit { protected bannerService: AccountDeprovisioningBannerService, private accountService: AccountService, private freeFamiliesPolicyService: FreeFamiliesPolicyService, + private organizationBillingService: OrganizationBillingServiceAbstraction, ) {} async ngOnInit() { @@ -148,6 +151,18 @@ export class OrganizationLayoutComponent implements OnInit { )) ? "claimedDomains" : "domainVerification"; + + this.canShowPoliciesTab$ = this.organization$.pipe( + switchMap((organization) => + this.organizationBillingService + .isBreadcrumbingPoliciesEnabled$(organization) + .pipe( + map( + (isBreadcrumbingEnabled) => isBreadcrumbingEnabled || organization.canManagePolicies, + ), + ), + ), + ); } canShowVaultTab(organization: Organization): boolean { diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.html b/apps/web/src/app/admin-console/organizations/policies/policies.component.html index 24021bb765f..e40b9d80e9e 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.html @@ -1,4 +1,17 @@ - + + @let organization = organization$ | async; + + diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts index 52cb4da107a..2b86d76d9b1 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts @@ -2,8 +2,8 @@ // @ts-strict-ignore import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { firstValueFrom, lastValueFrom } from "rxjs"; -import { first, map } from "rxjs/operators"; +import { firstValueFrom, lastValueFrom, map, Observable, switchMap } from "rxjs"; +import { first } from "rxjs/operators"; import { getOrganizationById, @@ -14,10 +14,17 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { DialogService } from "@bitwarden/components"; +import { + ChangePlanDialogResultType, + openChangePlanDialog, +} from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component"; +import { All } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { PolicyListService } from "../../core/policy-list.service"; import { BasePolicy } from "../policies"; +import { CollectionDialogTabType } from "../shared/components/collection-dialog"; import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component"; @@ -32,17 +39,19 @@ export class PoliciesComponent implements OnInit { loading = true; organizationId: string; policies: BasePolicy[]; - organization: Organization; + protected organization$: Observable; private orgPolicies: PolicyResponse[]; protected policiesEnabledMap: Map = new Map(); + protected isBreadcrumbingEnabled$: Observable; constructor( private route: ActivatedRoute, - private organizationService: OrganizationService, private accountService: AccountService, + private organizationService: OrganizationService, private policyApiService: PolicyApiServiceAbstraction, private policyListService: PolicyListService, + private organizationBillingService: OrganizationBillingServiceAbstraction, private dialogService: DialogService, ) {} @@ -53,11 +62,9 @@ export class PoliciesComponent implements OnInit { const userId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); - this.organization = await firstValueFrom( - this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(this.organizationId)), - ); + this.organization$ = this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)); this.policies = this.policyListService.getPolicies(); await this.load(); @@ -91,7 +98,11 @@ export class PoliciesComponent implements OnInit { this.orgPolicies.forEach((op) => { this.policiesEnabledMap.set(op.type, op.enabled); }); - + this.isBreadcrumbingEnabled$ = this.organization$.pipe( + switchMap((organization) => + this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization), + ), + ); this.loading = false; } @@ -104,8 +115,34 @@ export class PoliciesComponent implements OnInit { }); const result = await lastValueFrom(dialogRef.closed); - if (result === PolicyEditDialogResult.Saved) { - await this.load(); + switch (result) { + case PolicyEditDialogResult.Saved: + await this.load(); + break; + case PolicyEditDialogResult.UpgradePlan: + await this.changePlan(await firstValueFrom(this.organization$)); + break; } } + + protected readonly CollectionDialogTabType = CollectionDialogTabType; + protected readonly All = All; + + protected async changePlan(organization: Organization) { + const reference = openChangePlanDialog(this.dialogService, { + data: { + organizationId: organization.id, + subscription: null, + productTierType: organization.productTierType, + }, + }); + + const result = await lastValueFrom(reference.closed); + + if (result === ChangePlanDialogResultType.Closed) { + return; + } + + await this.load(); + } } diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html index 671083a2318..7f33f08f888 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html @@ -1,5 +1,17 @@
+ + +
+ + + diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts index f33460e8c16..49f4d15a100 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts @@ -9,12 +9,20 @@ import { ViewContainerRef, } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { Observable, map } from "rxjs"; +import { map, Observable, switchMap } from "rxjs"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DIALOG_DATA, @@ -35,6 +43,7 @@ export type PolicyEditDialogData = { export enum PolicyEditDialogResult { Saved = "saved", + UpgradePlan = "upgrade-plan", } @Component({ selector: "app-policy-edit", @@ -48,22 +57,28 @@ export class PolicyEditComponent implements AfterViewInit { loading = true; enabled = false; saveDisabled$: Observable; - defaultTypes: any[]; policyComponent: BasePolicyComponent; private policyResponse: PolicyResponse; formGroup = this.formBuilder.group({ enabled: [this.enabled], }); + protected organization$: Observable; + protected isBreadcrumbingEnabled$: Observable; + constructor( @Inject(DIALOG_DATA) protected data: PolicyEditDialogData, + private accountService: AccountService, private policyApiService: PolicyApiServiceAbstraction, + private organizationService: OrganizationService, private i18nService: I18nService, private cdr: ChangeDetectorRef, private formBuilder: FormBuilder, private dialogRef: DialogRef, private toastService: ToastService, + private organizationBillingService: OrganizationBillingServiceAbstraction, ) {} + get policy(): BasePolicy { return this.data.policy; } @@ -97,6 +112,16 @@ export class PolicyEditComponent implements AfterViewInit { throw e; } } + this.organization$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.organizationService.organizations$(userId)), + getOrganizationById(this.data.organizationId), + ); + this.isBreadcrumbingEnabled$ = this.organization$.pipe( + switchMap((organization) => + this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization), + ), + ); } submit = async () => { @@ -119,4 +144,8 @@ export class PolicyEditComponent implements AfterViewInit { static open = (dialogService: DialogService, config: DialogConfig) => { return dialogService.open(PolicyEditComponent, config); }; + + protected upgradePlan(): void { + this.dialogRef.close(PolicyEditDialogResult.UpgradePlan); + } } diff --git a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts index a644086628c..cfec0be531b 100644 --- a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts @@ -1,8 +1,10 @@ -import { NgModule } from "@angular/core"; +import { NgModule, inject } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { map } from "rxjs"; import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard"; import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard"; @@ -41,7 +43,14 @@ const routes: Routes = [ { path: "policies", component: PoliciesComponent, - canActivate: [organizationPermissionsGuard((org) => org.canManagePolicies)], + canActivate: [ + organizationPermissionsGuard((o: Organization) => { + const organizationBillingService = inject(OrganizationBillingServiceAbstraction); + return organizationBillingService + .isBreadcrumbingPoliciesEnabled$(o) + .pipe(map((isBreadcrumbingEnabled) => o.canManagePolicies || isBreadcrumbingEnabled)); + }), + ], data: { titleId: "policies", }, diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html index fdbb6dbba91..ca1264829b9 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html @@ -1,6 +1,7 @@
- + +
diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 0334519516a..fc8356505f5 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -58,7 +58,6 @@ import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component"; import { RecoverDeleteComponent } from "./auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component"; -import { RemovePasswordComponent } from "./auth/remove-password.component"; import { SetPasswordComponent } from "./auth/set-password.component"; import { AccountComponent } from "./auth/settings/account/account.component"; import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component"; @@ -73,6 +72,7 @@ import { CompleteTrialInitiationComponent } from "./billing/trial-initiation/com import { freeTrialTextResolver } from "./billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver"; import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component"; import { RouteDataProperties } from "./core"; +import { RemovePasswordComponent } from "./key-management/key-connector/remove-password.component"; import { FrontendLayoutComponent } from "./layouts/frontend-layout.component"; import { UserLayoutComponent } from "./layouts/user-layout.component"; import { RequestSMAccessComponent } from "./secrets-manager/secrets-manager-landing/request-sm-access.component"; diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 469ebe457d0..eb63b9f798c 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -15,7 +15,6 @@ import { VerifyRecoverDeleteOrgComponent } from "../admin-console/organizations/ import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations/sponsorships/accept-family-sponsorship.component"; import { RecoverDeleteComponent } from "../auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component"; -import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { AccountComponent } from "../auth/settings/account/account.component"; import { ChangeAvatarDialogComponent } from "../auth/settings/account/change-avatar-dialog.component"; @@ -42,6 +41,7 @@ import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-famili import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component"; import { DynamicAvatarComponent } from "../components/dynamic-avatar.component"; import { SelectableAvatarComponent } from "../components/selectable-avatar.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { FrontendLayoutComponent } from "../layouts/frontend-layout.component"; import { HeaderModule } from "../layouts/header/header.module"; import { ProductSwitcherModule } from "../layouts/product-switcher/product-switcher.module"; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 3d6cf0f23a5..56a98a661ef 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2,6 +2,9 @@ "allApplications": { "message": "All applications" }, + "appLogoLabel": { + "message": "Bitwarden logo" + }, "criticalApplications": { "message": "Critical applications" }, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.html index 30cd7fec8f2..3892892a9c6 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.html @@ -1,6 +1,10 @@
- +

- +

- +

{ const mockOrganizationId = "mockOrgId" as OrganizationId; @@ -112,5 +115,34 @@ describe("ImportService", () => { ]), ); }); + + it("should generate user report export items and include users with no access", async () => { + reportApiService.getMemberAccessData.mockImplementation(() => + Promise.resolve(memberAccessWithoutAccessDetailsReportsMock), + ); + const result = + await memberAccessReportService.generateUserReportExportItems(mockOrganizationId); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + email: "asmith@email.com", + name: "Alice Smith", + twoStepLogin: "memberAccessReportTwoFactorEnabledTrue", + accountRecovery: "memberAccessReportAuthenticationEnabledTrue", + group: "Alice Group 1", + totalItems: "10", + }), + expect.objectContaining({ + email: "rbrown@email.com", + name: "Robert Brown", + twoStepLogin: "memberAccessReportTwoFactorEnabledFalse", + accountRecovery: "memberAccessReportAuthenticationEnabledFalse", + group: "memberAccessReportNoGroup", + totalItems: "0", + }), + ]), + ); + }); }); }); diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts index b7ff5551e2c..029dce8a404 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts @@ -65,6 +65,26 @@ export class MemberAccessReportService { } const exportItems = memberAccessReports.flatMap((report) => { + // to include users without access details + // which means a user has no groups, collections or items + if (report.accessDetails.length === 0) { + return [ + { + email: report.email, + name: report.userName, + twoStepLogin: report.twoFactorEnabled + ? this.i18nService.t("memberAccessReportTwoFactorEnabledTrue") + : this.i18nService.t("memberAccessReportTwoFactorEnabledFalse"), + accountRecovery: report.accountRecoveryEnabled + ? this.i18nService.t("memberAccessReportAuthenticationEnabledTrue") + : this.i18nService.t("memberAccessReportAuthenticationEnabledFalse"), + group: this.i18nService.t("memberAccessReportNoGroup"), + collection: this.i18nService.t("memberAccessReportNoCollection"), + collectionPermission: this.i18nService.t("memberAccessReportNoCollectionPermission"), + totalItems: "0", + }, + ]; + } const userDetails = report.accessDetails.map((detail) => { const collectionName = collectionNameMap.get(detail.collectionName.encryptedString); return { diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 3cce9b5357e..8e2b3409593 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1255,6 +1255,7 @@ const safeProviders: SafeProvider[] = [ I18nServiceAbstraction, OrganizationApiServiceAbstraction, SyncService, + ConfigService, ], }), safeProvider({ diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index f31a5500b43..1e16dba82cc 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -10,7 +10,7 @@ [routerLink]="['/']" class="tw-w-[128px] tw-block tw-mb-12 [&>*]:tw-align-top" > - +

Promise; + + /** + * Determines if breadcrumbing policies is enabled for the organizations meeting certain criteria. + * @param organization + */ + abstract isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable; } diff --git a/libs/common/src/billing/services/organization-billing.service.spec.ts b/libs/common/src/billing/services/organization-billing.service.spec.ts new file mode 100644 index 00000000000..7b194dff637 --- /dev/null +++ b/libs/common/src/billing/services/organization-billing.service.spec.ts @@ -0,0 +1,149 @@ +import { mock } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; +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 { SyncService } from "@bitwarden/common/platform/sync"; +import { KeyService } from "@bitwarden/key-management"; + +describe("BillingAccountProfileStateService", () => { + let apiService: jest.Mocked; + let billingApiService: jest.Mocked; + let keyService: jest.Mocked; + let encryptService: jest.Mocked; + let i18nService: jest.Mocked; + let organizationApiService: jest.Mocked; + let syncService: jest.Mocked; + let configService: jest.Mocked; + + let sut: OrganizationBillingService; + + beforeEach(() => { + apiService = mock(); + billingApiService = mock(); + keyService = mock(); + encryptService = mock(); + i18nService = mock(); + organizationApiService = mock(); + syncService = mock(); + configService = mock(); + + sut = new OrganizationBillingService( + apiService, + billingApiService, + keyService, + encryptService, + i18nService, + organizationApiService, + syncService, + configService, + ); + }); + + afterEach(() => { + return jest.resetAllMocks(); + }); + + describe("isBreadcrumbingPoliciesEnabled", () => { + it("returns false when feature flag is disabled", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: ProductTierType.Teams, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + expect(configService.getFeatureFlag$).toHaveBeenCalledWith( + FeatureFlag.PM12276_BreadcrumbEventLogs, + ); + }); + + it("returns false when organization belongs to a provider", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: true, + canEditSubscription: true, + productTierType: ProductTierType.Teams, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it("returns false when cannot edit subscription", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: false, + canEditSubscription: false, + productTierType: ProductTierType.Teams, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it.each([ + ["Teams", ProductTierType.Teams], + ["TeamsStarter", ProductTierType.TeamsStarter], + ])("returns true when all conditions are met with %s tier", async (_, productTierType) => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: productTierType, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(true); + expect(configService.getFeatureFlag$).toHaveBeenCalledWith( + FeatureFlag.PM12276_BreadcrumbEventLogs, + ); + }); + + it("returns false when product tier is not supported", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: ProductTierType.Enterprise, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it("handles all conditions false correctly", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + const org = { + isProviderUser: true, + canEditSubscription: false, + productTierType: ProductTierType.Free, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it("verifies feature flag is only called once", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: ProductTierType.Teams, + } as Organization; + + await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(configService.getFeatureFlag$).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index 83efbf0a30c..6622cdcdce3 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -1,5 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { Observable, of, switchMap } from "rxjs"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { KeyService } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; @@ -20,7 +25,7 @@ import { PlanInformation, SubscriptionInformation, } from "../abstractions"; -import { PlanType } from "../enums"; +import { PlanType, ProductTierType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; import { PaymentSourceResponse } from "../models/response/payment-source.response"; @@ -40,6 +45,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private i18nService: I18nService, private organizationApiService: OrganizationApiService, private syncService: SyncService, + private configService: ConfigService, ) {} async getPaymentSource(organizationId: string): Promise { @@ -220,4 +226,29 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs this.setPaymentInformation(request, subscription.payment); await this.billingApiService.restartSubscription(organizationId, request); } + + isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable { + if (organization === null || organization === undefined) { + return of(false); + } + + return this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs).pipe( + switchMap((featureFlagEnabled) => { + if (!featureFlagEnabled) { + return of(false); + } + + if (organization.isProviderUser || !organization.canEditSubscription) { + return of(false); + } + + const supportedProducts = [ProductTierType.Teams, ProductTierType.TeamsStarter]; + const isSupportedProduct = supportedProducts.some( + (product) => product === organization.productTierType, + ); + + return of(isSupportedProduct); + }), + ); + } } diff --git a/libs/common/src/models/view/view.ts b/libs/common/src/models/view/view.ts index 1f16b3d5958..2869617dca5 100644 --- a/libs/common/src/models/view/view.ts +++ b/libs/common/src/models/view/view.ts @@ -1 +1,5 @@ +// See https://contributing.bitwarden.com/architecture/clients/data-model/#view for proper use. +// View models represent the decrypted state of a corresponding Domain model. +// They typically match the Domain model but contains a decrypted string for any EncString fields. +// Don't use this to represent arbitrary component view data as that isn't what it is for. export class View {} diff --git a/libs/common/src/platform/misc/utils.spec.ts b/libs/common/src/platform/misc/utils.spec.ts index 964a2a19413..818138863fb 100644 --- a/libs/common/src/platform/misc/utils.spec.ts +++ b/libs/common/src/platform/misc/utils.spec.ts @@ -706,4 +706,73 @@ describe("Utils Service", () => { }); }); }); + + describe("fromUtf8ToB64(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should handle empty string", () => { + const str = Utils.fromUtf8ToB64(""); + expect(str).toBe(""); + }); + + runInBothEnvironments("should convert a normal b64 string", () => { + const str = Utils.fromUtf8ToB64(asciiHelloWorld); + expect(str).toBe(b64HelloWorldString); + }); + + runInBothEnvironments("should convert various special characters", () => { + const cases = [ + { input: "»", output: "wrs=" }, + { input: "¦", output: "wqY=" }, + { input: "£", output: "wqM=" }, + { input: "é", output: "w6k=" }, + { input: "ö", output: "w7Y=" }, + { input: "»»", output: "wrvCuw==" }, + ]; + cases.forEach((c) => { + const utfStr = c.input; + const str = Utils.fromUtf8ToB64(utfStr); + expect(str).toBe(c.output); + }); + }); + }); + + describe("fromB64ToUtf8(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should handle empty string", () => { + const str = Utils.fromB64ToUtf8(""); + expect(str).toBe(""); + }); + + runInBothEnvironments("should convert a normal b64 string", () => { + const str = Utils.fromB64ToUtf8(b64HelloWorldString); + expect(str).toBe(asciiHelloWorld); + }); + + runInBothEnvironments("should handle various special characters", () => { + const cases = [ + { input: "wrs=", output: "»" }, + { input: "wqY=", output: "¦" }, + { input: "wqM=", output: "£" }, + { input: "w6k=", output: "é" }, + { input: "w7Y=", output: "ö" }, + { input: "wrvCuw==", output: "»»" }, + ]; + + cases.forEach((c) => { + const b64Str = c.input; + const str = Utils.fromB64ToUtf8(b64Str); + expect(str).toBe(c.output); + }); + }); + }); }); diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index ef65d2130a0..203a04851c5 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -233,7 +233,7 @@ export class Utils { if (Utils.isNode) { return Buffer.from(utfStr, "utf8").toString("base64"); } else { - return decodeURIComponent(escape(Utils.global.btoa(utfStr))); + return BufferLib.from(utfStr, "utf8").toString("base64"); } } @@ -245,7 +245,7 @@ export class Utils { if (Utils.isNode) { return Buffer.from(b64Str, "base64").toString("utf8"); } else { - return decodeURIComponent(escape(Utils.global.atob(b64Str))); + return BufferLib.from(b64Str, "base64").toString("utf8"); } } diff --git a/libs/components/src/icon/icon.component.ts b/libs/components/src/icon/icon.component.ts index 2382d197bec..08fa25956d0 100644 --- a/libs/components/src/icon/icon.component.ts +++ b/libs/components/src/icon/icon.component.ts @@ -1,19 +1,23 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, HostBinding, Input } from "@angular/core"; +import { Component, Input } from "@angular/core"; import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; import { Icon, isIcon } from "./icon"; @Component({ selector: "bit-icon", + host: { + "[attr.aria-hidden]": "!ariaLabel", + "[attr.aria-label]": "ariaLabel", + "[innerHtml]": "innerHtml", + }, template: ``, standalone: true, }) export class BitIconComponent { + innerHtml: SafeHtml | null = null; + @Input() set icon(icon: Icon) { if (!isIcon(icon)) { - this.innerHtml = ""; return; } @@ -21,7 +25,7 @@ export class BitIconComponent { this.innerHtml = this.domSanitizer.bypassSecurityTrustHtml(svg); } - @HostBinding() innerHtml: SafeHtml; + @Input() ariaLabel: string | undefined = undefined; constructor(private domSanitizer: DomSanitizer) {} } diff --git a/libs/components/src/icon/icon.mdx b/libs/components/src/icon/icon.mdx index fc1c4cd3d57..6435fc24948 100644 --- a/libs/components/src/icon/icon.mdx +++ b/libs/components/src/icon/icon.mdx @@ -98,9 +98,19 @@ import * as stories from "./icon.stories"; ``` - **HTML:** + + > NOTE: SVG icons are treated as decorative by default and will be `aria-hidden` unless an + > `ariaLabel` is explicitly provided to the `` component + ```html ``` + With `ariaLabel` + + ```html + + ``` + 8. **Ensure your SVG renders properly** according to Figma in both light and dark modes on a client which supports multiple style modes. diff --git a/libs/components/src/icon/icon.stories.ts b/libs/components/src/icon/icon.stories.ts index 53454567b7f..7892bdd3ec1 100644 --- a/libs/components/src/icon/icon.stories.ts +++ b/libs/components/src/icon/icon.stories.ts @@ -26,5 +26,9 @@ export const Default: Story = { mapping: GenericIcons, control: { type: "select" }, }, + ariaLabel: { + control: "text", + description: "the text used by a screen reader to describe the icon", + }, }, }; diff --git a/libs/key-management-ui/src/index.ts b/libs/key-management-ui/src/index.ts index 2f98538caad..b330e390d36 100644 --- a/libs/key-management-ui/src/index.ts +++ b/libs/key-management-ui/src/index.ts @@ -7,3 +7,4 @@ export { LockComponentService, UnlockOptions } from "./lock/services/lock-compon export { KeyRotationTrustInfoComponent } from "./key-rotation/key-rotation-trust-info.component"; export { AccountRecoveryTrustComponent } from "./trust/account-recovery-trust.component"; export { EmergencyAccessTrustComponent } from "./trust/emergency-access-trust.component"; +export { RemovePasswordComponent } from "./key-connector/remove-password.component"; diff --git a/libs/angular/src/auth/components/remove-password.component.ts b/libs/key-management-ui/src/key-connector/remove-password.component.ts similarity index 100% rename from libs/angular/src/auth/components/remove-password.component.ts rename to libs/key-management-ui/src/key-connector/remove-password.component.ts diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 4e9b4175838..71599c19ae0 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -220,7 +220,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { this.exportForm.controls.vaultSelector.valueChanges .pipe(takeUntil(this.destroy$)) - .subscribe(([value]) => { + .subscribe((value) => { this.organizationId = value !== "myVault" ? value : undefined; this.formatOptions = this.formatOptions.filter((option) => option.value !== "zip");