mirror of
https://github.com/bitwarden/browser
synced 2026-02-11 05:53:42 +00:00
Merge remote-tracking branch 'origin' into auth/pm-18720/change-password-component-non-dialog-v3
This commit is contained in:
@@ -13,8 +13,10 @@ import {
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
@@ -25,12 +27,14 @@ describe("AuthGuard", () => {
|
||||
authStatus: AuthenticationStatus,
|
||||
forceSetPasswordReason: ForceSetPasswordReason,
|
||||
keyConnectorServiceRequiresAccountConversion: boolean = false,
|
||||
featureFlag: FeatureFlag | null = null,
|
||||
) => {
|
||||
const authService: MockProxy<AuthService> = mock<AuthService>();
|
||||
authService.getAuthStatus.mockResolvedValue(authStatus);
|
||||
const messagingService: MockProxy<MessagingService> = mock<MessagingService>();
|
||||
const keyConnectorService: MockProxy<KeyConnectorService> = mock<KeyConnectorService>();
|
||||
keyConnectorService.convertAccountRequired$ = of(keyConnectorServiceRequiresAccountConversion);
|
||||
const configService: MockProxy<ConfigService> = mock<ConfigService>();
|
||||
const accountService: MockProxy<AccountService> = mock<AccountService>();
|
||||
const activeAccountSubject = new BehaviorSubject<Account | null>(null);
|
||||
accountService.activeAccount$ = activeAccountSubject;
|
||||
@@ -45,6 +49,12 @@ describe("AuthGuard", () => {
|
||||
),
|
||||
);
|
||||
|
||||
if (featureFlag) {
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
} else {
|
||||
configService.getFeatureFlag.mockResolvedValue(false);
|
||||
}
|
||||
|
||||
const forceSetPasswordReasonSubject = new BehaviorSubject<ForceSetPasswordReason>(
|
||||
forceSetPasswordReason,
|
||||
);
|
||||
@@ -59,7 +69,10 @@ describe("AuthGuard", () => {
|
||||
{ path: "guarded-route", component: EmptyComponent, canActivate: [authGuard] },
|
||||
{ path: "lock", component: EmptyComponent },
|
||||
{ path: "set-password", component: EmptyComponent },
|
||||
{ path: "set-password-jit", component: EmptyComponent },
|
||||
{ path: "set-initial-password", component: EmptyComponent },
|
||||
{ path: "update-temp-password", component: EmptyComponent },
|
||||
{ path: "change-password", component: EmptyComponent },
|
||||
{ path: "remove-password", component: EmptyComponent },
|
||||
]),
|
||||
],
|
||||
@@ -69,6 +82,7 @@ describe("AuthGuard", () => {
|
||||
{ provide: KeyConnectorService, useValue: keyConnectorService },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: MasterPasswordServiceAbstraction, useValue: masterPasswordService },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -110,70 +124,152 @@ describe("AuthGuard", () => {
|
||||
expect(router.url).toBe("/remove-password");
|
||||
});
|
||||
|
||||
it("should redirect to set-password when user is TDE user without password and has password reset permission", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
);
|
||||
describe("given user is Unlocked", () => {
|
||||
describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.SsoNewJitProvisionedUser,
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
ForceSetPasswordReason.TdeOffboarding,
|
||||
];
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/set-password");
|
||||
});
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
reason,
|
||||
false,
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
|
||||
it("should redirect to update-temp-password when user has force set password reason", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
);
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/update-temp-password");
|
||||
});
|
||||
describe("given user attempts to navigate to /set-initial-password", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
reason,
|
||||
false,
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
|
||||
it("should redirect to update-temp-password when user has weak password", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
);
|
||||
await router.navigate(["/set-initial-password"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/update-temp-password");
|
||||
});
|
||||
describe("given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => {
|
||||
const tests = [
|
||||
{
|
||||
reason: ForceSetPasswordReason.SsoNewJitProvisionedUser,
|
||||
url: "/set-password-jit",
|
||||
},
|
||||
{
|
||||
reason: ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
url: "/set-password",
|
||||
},
|
||||
{
|
||||
reason: ForceSetPasswordReason.TdeOffboarding,
|
||||
url: "/update-temp-password",
|
||||
},
|
||||
];
|
||||
|
||||
it("should allow navigation to set-password when the user is unlocked, is a TDE user without password, and has password reset permission", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
);
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach(({ reason, url }) => {
|
||||
it(`should redirect to ${url} when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate(["/set-password"]);
|
||||
expect(router.url).toContain("/set-password");
|
||||
});
|
||||
await router.navigate(["/guarded-route"]);
|
||||
expect(router.url).toContain(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow navigation to update-temp-password when the user is unlocked and has admin force password reset permission", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
);
|
||||
describe("given user attempts to navigate to the set- or update- password route itself", () => {
|
||||
tests.forEach(({ reason, url }) => {
|
||||
it(`should allow navigation to continue to ${url} when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate(["/update-temp-password"]);
|
||||
expect(router.url).toContain("/update-temp-password");
|
||||
});
|
||||
await router.navigate([url]);
|
||||
expect(router.url).toContain(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow navigation to update-temp-password when the user is unlocked and has weak password", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
);
|
||||
describe("given the PM16117_ChangeExistingPasswordRefactor feature flag is ON", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
];
|
||||
|
||||
await router.navigate(["/update-temp-password"]);
|
||||
expect(router.url).toContain("/update-temp-password");
|
||||
});
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /change-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
reason,
|
||||
false,
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
|
||||
it("should allow navigation to remove-password when the user is unlocked and has 'none' password reset permission", async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, ForceSetPasswordReason.None);
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/change-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await router.navigate(["/remove-password"]);
|
||||
expect(router.url).toContain("/remove-password");
|
||||
describe("given user attempts to navigate to /change-password", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should allow navigation to /change-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
false,
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
|
||||
await router.navigate(["/change-password"]);
|
||||
expect(router.url).toContain("/change-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given the PM16117_ChangeExistingPasswordRefactor feature flag is OFF", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
];
|
||||
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /update-temp-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/update-temp-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given user attempts to navigate to /update-temp-password", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should allow navigation to continue to /update-temp-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate(["/update-temp-password"]);
|
||||
expect(router.url).toContain("/update-temp-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,15 +60,45 @@ export const authGuard: CanActivateFn = async (
|
||||
masterPasswordService.forceSetPasswordReason$(userId),
|
||||
);
|
||||
|
||||
const isSetInitialPasswordFlagOn = await configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
const isChangePasswordFlagOn = await configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
|
||||
// User JIT provisioned into a master-password-encryption org
|
||||
if (
|
||||
forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser &&
|
||||
!routerState.url.includes("set-password-jit") &&
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/set-password-jit";
|
||||
return router.createUrlTree([route]);
|
||||
}
|
||||
|
||||
// TDE org user has "manage account recovery" permission
|
||||
if (
|
||||
forceSetPasswordReason ===
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission &&
|
||||
!routerState.url.includes("set-password")
|
||||
!routerState.url.includes("set-password") &&
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
return router.createUrlTree(["/set-password"]);
|
||||
const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/set-password";
|
||||
return router.createUrlTree([route]);
|
||||
}
|
||||
|
||||
if (await configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)) {
|
||||
// TDE Offboarding
|
||||
if (
|
||||
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding &&
|
||||
!routerState.url.includes("update-temp-password") &&
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/update-temp-password";
|
||||
return router.createUrlTree([route]);
|
||||
}
|
||||
|
||||
if (isChangePasswordFlagOn) {
|
||||
// When the PM16117_ChangeExistingPasswordRefactor flag is removed AS WELL AS the cleanup for
|
||||
// update-temp-password also remove the conditional check for update-temp-password here.
|
||||
// That route will no longer be in effect.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from "./auth.guard";
|
||||
export * from "./active-auth.guard";
|
||||
export * from "./lock.guard";
|
||||
export * from "./redirect.guard";
|
||||
export * from "./redirect/redirect.guard";
|
||||
export * from "./tde-decryption-required.guard";
|
||||
export * from "./unauth.guard";
|
||||
|
||||
53
libs/angular/src/auth/guards/redirect/README.md
Normal file
53
libs/angular/src/auth/guards/redirect/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Redirect Guard
|
||||
|
||||
The `redirectGuard` redirects the user based on their `AuthenticationStatus`. It is applied to the root route (`/`).
|
||||
|
||||
<br>
|
||||
|
||||
### Order of Operations
|
||||
|
||||
The `redirectGuard` will redirect the user based on the following checks, _in order_:
|
||||
|
||||
- **`AuthenticationStatus.LoggedOut`** → redirect to `/login`
|
||||
- **`AuthenticationStatus.Unlocked`** → redirect to `/vault`
|
||||
- **`AuthenticationStatus.Locked`**
|
||||
- **TDE Locked State** → redirect to `/login-initiated`
|
||||
- A user is in a TDE Locked State if they meet all 3 of the following conditions
|
||||
1. Auth status is `Locked`
|
||||
2. TDE is enabled
|
||||
3. User has never had a user key (that is, user has not unlocked/decrypted yet)
|
||||
- **Standard Locked State** → redirect to `/lock`
|
||||
|
||||
<br>
|
||||
|
||||
| Order | AuthenticationStatus | Redirect To |
|
||||
| ----- | ------------------------------------------------------------------------------- | ------------------ |
|
||||
| 1 | `LoggedOut` | `/login` |
|
||||
| 2 | `Unlocked` | `/vault` |
|
||||
| 3 | **TDE Locked State** <br> `Locked` + <br> `tdeEnabled` + <br> `!everHadUserKey` | `/login-initiated` |
|
||||
| 4 | **Standard Locked State** <br> `Locked` | `/lock` |
|
||||
|
||||
<br>
|
||||
|
||||
### Default Routes and Route Overrides
|
||||
|
||||
The default redirect routes are mapped to object properties:
|
||||
|
||||
```typescript
|
||||
const defaultRoutes: RedirectRoutes = {
|
||||
loggedIn: "/vault",
|
||||
loggedOut: "/login",
|
||||
locked: "/lock",
|
||||
notDecrypted: "/login-initiated",
|
||||
};
|
||||
```
|
||||
|
||||
But when applying the guard to the root route, the developer can override specific redirect routes by passing in a custom object. This is useful for subtle differences in client-specific routing:
|
||||
|
||||
```typescript
|
||||
// app-routing.module.ts (Browser Extension)
|
||||
{
|
||||
path: "",
|
||||
canActivate: [redirectGuard({ loggedIn: "/tabs/current"})],
|
||||
}
|
||||
```
|
||||
@@ -25,12 +25,14 @@ const defaultRoutes: RedirectRoutes = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Guard that consolidates all redirection logic, should be applied to root route.
|
||||
* Redirects the user to the appropriate route based on their `AuthenticationStatus`.
|
||||
* This guard should be applied to the root route.
|
||||
*
|
||||
* TODO: This should return Observable<boolean | UrlTree> once we can get rid of all the promises
|
||||
*/
|
||||
export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActivateFn {
|
||||
const routes = { ...defaultRoutes, ...overrides };
|
||||
|
||||
return async (route) => {
|
||||
const authService = inject(AuthService);
|
||||
const keyService = inject(KeyService);
|
||||
@@ -41,16 +43,21 @@ export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActiv
|
||||
|
||||
const authStatus = await authService.getAuthStatus();
|
||||
|
||||
// Logged Out
|
||||
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||
return router.createUrlTree([routes.loggedOut], { queryParams: route.queryParams });
|
||||
}
|
||||
|
||||
// Unlocked
|
||||
if (authStatus === AuthenticationStatus.Unlocked) {
|
||||
return router.createUrlTree([routes.loggedIn], { queryParams: route.queryParams });
|
||||
}
|
||||
|
||||
// If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the
|
||||
// login decryption options component.
|
||||
// Locked: TDE Locked State
|
||||
// - If user meets all 3 of the following conditions:
|
||||
// 1. Auth status is Locked
|
||||
// 2. TDE is enabled
|
||||
// 3. User has never had a user key (has not decrypted yet)
|
||||
const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$);
|
||||
const userId = await firstValueFrom(accountService.activeAccount$.pipe(getUserId));
|
||||
const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$(userId));
|
||||
@@ -64,6 +71,7 @@ export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActiv
|
||||
return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams });
|
||||
}
|
||||
|
||||
// Locked: Standard Locked State
|
||||
if (authStatus === AuthenticationStatus.Locked) {
|
||||
return router.createUrlTree([routes.locked], { queryParams: route.queryParams });
|
||||
}
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
DefaultAnonLayoutWrapperDataService,
|
||||
DefaultLoginApprovalComponentService,
|
||||
DefaultLoginComponentService,
|
||||
DefaultLoginDecryptionOptionsService,
|
||||
@@ -42,6 +40,7 @@ import {
|
||||
AuthRequestServiceAbstraction,
|
||||
DefaultAuthRequestApiService,
|
||||
DefaultLoginSuccessHandlerService,
|
||||
DefaultLogoutService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
LoginApprovalComponentServiceAbstraction,
|
||||
LoginEmailService,
|
||||
@@ -50,6 +49,7 @@ import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginSuccessHandlerService,
|
||||
LogoutReason,
|
||||
LogoutService,
|
||||
PinService,
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
@@ -294,10 +294,15 @@ import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services
|
||||
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service";
|
||||
import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
DefaultAnonLayoutWrapperDataService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
GeneratorHistoryService,
|
||||
LocalGeneratorHistoryService,
|
||||
@@ -405,6 +410,7 @@ const safeProviders: SafeProvider[] = [
|
||||
provide: STATE_FACTORY,
|
||||
useValue: new StateFactory(GlobalState, Account),
|
||||
}),
|
||||
// TODO: PM-21212 - Deprecate LogoutCallback in favor of LogoutService
|
||||
safeProvider({
|
||||
provide: LOGOUT_CALLBACK,
|
||||
useFactory:
|
||||
@@ -676,6 +682,11 @@ const safeProviders: SafeProvider[] = [
|
||||
KdfConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RestrictedItemTypesService,
|
||||
useClass: RestrictedItemTypesService,
|
||||
deps: [ConfigService, AccountService, OrganizationServiceAbstraction, PolicyServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PasswordStrengthServiceAbstraction,
|
||||
useClass: PasswordStrengthService,
|
||||
@@ -881,6 +892,7 @@ const safeProviders: SafeProvider[] = [
|
||||
KdfConfigService,
|
||||
AccountServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
RestrictedItemTypesService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -896,6 +908,7 @@ const safeProviders: SafeProvider[] = [
|
||||
CollectionService,
|
||||
KdfConfigService,
|
||||
AccountServiceAbstraction,
|
||||
RestrictedItemTypesService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1540,6 +1553,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: MasterPasswordApiService,
|
||||
deps: [ApiServiceAbstraction, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LogoutService,
|
||||
useClass: DefaultLogoutService,
|
||||
deps: [MessagingServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DocumentLangSetter,
|
||||
useClass: DocumentLangSetter,
|
||||
|
||||
@@ -84,7 +84,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
showCardNumber = false;
|
||||
showCardCode = false;
|
||||
cipherType = CipherType;
|
||||
typeOptions: any[];
|
||||
cardBrandOptions: any[];
|
||||
cardExpMonthOptions: any[];
|
||||
identityTitleOptions: any[];
|
||||
@@ -139,13 +138,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
protected sdkService: SdkService,
|
||||
private sshImportPromptService: SshImportPromptService,
|
||||
) {
|
||||
this.typeOptions = [
|
||||
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
|
||||
{ name: i18nService.t("typeCard"), value: CipherType.Card },
|
||||
{ name: i18nService.t("typeIdentity"), value: CipherType.Identity },
|
||||
{ name: i18nService.t("typeSecureNote"), value: CipherType.SecureNote },
|
||||
];
|
||||
|
||||
this.cardBrandOptions = [
|
||||
{ name: "-- " + i18nService.t("select") + " --", value: null },
|
||||
{ name: "Visa", value: "Visa" },
|
||||
@@ -215,8 +207,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.writeableCollections = await this.loadCollections();
|
||||
this.canUseReprompt = await this.passwordRepromptService.enabled();
|
||||
|
||||
this.typeOptions.push({ name: this.i18nService.t("typeSshKey"), value: CipherType.SshKey });
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
combineLatest,
|
||||
filter,
|
||||
from,
|
||||
map,
|
||||
of,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
@@ -20,6 +22,8 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||
|
||||
@Directive()
|
||||
export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
@@ -35,6 +39,19 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
organization: Organization;
|
||||
CipherType = CipherType;
|
||||
|
||||
protected itemTypes$ = this.restrictedItemTypesService.restricted$.pipe(
|
||||
map((restrictedItemTypes) =>
|
||||
// Filter out restricted item types
|
||||
CIPHER_MENU_ITEMS.filter(
|
||||
(itemType) =>
|
||||
!restrictedItemTypes.some(
|
||||
(restrictedType) => restrictedType.cipherType === itemType.type,
|
||||
),
|
||||
),
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
protected searchPending = false;
|
||||
|
||||
/** Construct filters as an observable so it can be appended to the cipher stream. */
|
||||
@@ -62,6 +79,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
protected searchService: SearchService,
|
||||
protected cipherService: CipherService,
|
||||
protected accountService: AccountService,
|
||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||
) {
|
||||
this.subscribeToCiphers();
|
||||
}
|
||||
@@ -143,18 +161,22 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
this._searchText$,
|
||||
this._filter$,
|
||||
of(userId),
|
||||
this.restrictedItemTypesService.restricted$,
|
||||
]),
|
||||
),
|
||||
switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId]) => {
|
||||
switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId, restricted]) => {
|
||||
let allCiphers = indexedCiphers ?? [];
|
||||
const _failedCiphers = failedCiphers ?? [];
|
||||
|
||||
allCiphers = [..._failedCiphers, ...allCiphers];
|
||||
|
||||
const restrictedTypeFilter = (cipher: CipherView) =>
|
||||
!this.restrictedItemTypesService.isCipherRestricted(cipher, restricted);
|
||||
|
||||
return this.searchService.searchCiphers(
|
||||
userId,
|
||||
searchText,
|
||||
[filter, this.deletedFilter],
|
||||
[filter, this.deletedFilter, restrictedTypeFilter],
|
||||
allCiphers,
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { Injectable, inject } from "@angular/core";
|
||||
import { Observable, combineLatest, from, of } from "rxjs";
|
||||
import { catchError, map } from "rxjs/operators";
|
||||
import { catchError, switchMap } from "rxjs/operators";
|
||||
|
||||
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
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 { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
import { NudgeStatus, NudgeType } from "../nudges.service";
|
||||
@@ -21,6 +25,9 @@ export class AccountSecurityNudgeService extends DefaultSingleNudgeService {
|
||||
private logService = inject(LogService);
|
||||
private pinService = inject(PinServiceAbstraction);
|
||||
private vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
|
||||
private biometricStateService = inject(BiometricStateService);
|
||||
private policyService = inject(PolicyService);
|
||||
private organizationService = inject(OrganizationService);
|
||||
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
|
||||
@@ -36,16 +43,45 @@ export class AccountSecurityNudgeService extends DefaultSingleNudgeService {
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
of(Date.now() - THIRTY_DAYS_MS),
|
||||
from(this.pinService.isPinSet(userId)),
|
||||
from(this.vaultTimeoutSettingsService.isBiometricLockSet(userId)),
|
||||
this.biometricStateService.biometricUnlockEnabled$,
|
||||
this.organizationService.organizations$(userId),
|
||||
this.policyService.policiesByType$(PolicyType.RemoveUnlockWithPin, userId),
|
||||
]).pipe(
|
||||
map(([profileCreationDate, status, profileCutoff, isPinSet, isBiometricLockSet]) => {
|
||||
const profileOlderThanCutoff = profileCreationDate.getTime() < profileCutoff;
|
||||
const hideNudge = profileOlderThanCutoff || isPinSet || isBiometricLockSet;
|
||||
return {
|
||||
hasBadgeDismissed: status.hasBadgeDismissed || hideNudge,
|
||||
hasSpotlightDismissed: status.hasSpotlightDismissed || hideNudge,
|
||||
};
|
||||
}),
|
||||
switchMap(
|
||||
async ([
|
||||
profileCreationDate,
|
||||
status,
|
||||
profileCutoff,
|
||||
isPinSet,
|
||||
biometricUnlockEnabled,
|
||||
organizations,
|
||||
policies,
|
||||
]) => {
|
||||
const profileOlderThanCutoff = profileCreationDate.getTime() < profileCutoff;
|
||||
|
||||
const hasOrgWithRemovePinPolicyOn = organizations.some((org) => {
|
||||
return policies.some(
|
||||
(p) => p.type === PolicyType.RemoveUnlockWithPin && p.organizationId === org.id,
|
||||
);
|
||||
});
|
||||
|
||||
const hideNudge =
|
||||
profileOlderThanCutoff ||
|
||||
isPinSet ||
|
||||
biometricUnlockEnabled ||
|
||||
hasOrgWithRemovePinPolicyOn;
|
||||
|
||||
const acctSecurityNudgeStatus = {
|
||||
hasBadgeDismissed: status.hasBadgeDismissed || hideNudge,
|
||||
hasSpotlightDismissed: status.hasSpotlightDismissed || hideNudge,
|
||||
};
|
||||
|
||||
if (isPinSet || biometricUnlockEnabled || hasOrgWithRemovePinPolicyOn) {
|
||||
await this.setNudgeStatus(nudgeType, acctSecurityNudgeStatus, userId);
|
||||
}
|
||||
return acctSecurityNudgeStatus;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { firstValueFrom, of } from "rxjs";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -13,6 +15,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../../../libs/common/spec";
|
||||
|
||||
@@ -91,6 +94,18 @@ describe("Vault Nudges Service", () => {
|
||||
provide: VaultTimeoutSettingsService,
|
||||
useValue: mock<VaultTimeoutSettingsService>(),
|
||||
},
|
||||
{
|
||||
provide: BiometricStateService,
|
||||
useValue: mock<BiometricStateService>(),
|
||||
},
|
||||
{
|
||||
provide: PolicyService,
|
||||
useValue: mock<PolicyService>(),
|
||||
},
|
||||
{
|
||||
provide: OrganizationService,
|
||||
useValue: mock<OrganizationService>(),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -159,7 +159,11 @@ export class NudgesService {
|
||||
*/
|
||||
hasActiveBadges$(userId: UserId): Observable<boolean> {
|
||||
// Add more nudge types here if they have the settings badge feature
|
||||
const nudgeTypes = [NudgeType.EmptyVaultNudge, NudgeType.DownloadBitwarden];
|
||||
const nudgeTypes = [
|
||||
NudgeType.EmptyVaultNudge,
|
||||
NudgeType.DownloadBitwarden,
|
||||
NudgeType.AutofillNudge,
|
||||
];
|
||||
|
||||
const nudgeTypesWithBadge$ = nudgeTypes.map((nudge) => {
|
||||
return this.getNudgeService(nudge)
|
||||
|
||||
Reference in New Issue
Block a user