1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 09:43:23 +00:00

[PM-5189] Merging main into current branch

This commit is contained in:
Cesar Gonzalez
2024-03-21 11:18:00 -05:00
141 changed files with 1900 additions and 469 deletions

View File

@@ -191,6 +191,15 @@
]
}
},
{
"files": ["libs/tools/export/vault-export/vault-export-ui/src/**/*.ts"],
"rules": {
"no-restricted-imports": [
"error",
{ "patterns": ["@bitwarden/vault-export-ui/*", "src/**/*"] }
]
}
},
{
"files": ["libs/importer/src/**/*.ts"],
"rules": {

View File

@@ -15,8 +15,8 @@ import {
factory,
} from "../../../platform/background/service-factories/factory-options";
import {
messagingServiceFactory,
MessagingServiceInitOptions,
messagingServiceFactory,
} from "../../../platform/background/service-factories/messaging-service.factory";
import {
StateServiceInitOptions,

View File

@@ -43,6 +43,11 @@ import {
stateServiceFactory,
} from "../../../platform/background/service-factories/state-service.factory";
import {
UserDecryptionOptionsServiceInitOptions,
userDecryptionOptionsServiceFactory,
} from "./user-decryption-options-service.factory";
type DeviceTrustCryptoServiceFactoryOptions = FactoryOptions;
export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactoryOptions &
@@ -54,7 +59,8 @@ export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactor
AppIdServiceInitOptions &
DevicesApiServiceInitOptions &
I18nServiceInitOptions &
PlatformUtilsServiceInitOptions;
PlatformUtilsServiceInitOptions &
UserDecryptionOptionsServiceInitOptions;
export function deviceTrustCryptoServiceFactory(
cache: { deviceTrustCryptoService?: DeviceTrustCryptoServiceAbstraction } & CachedServices,
@@ -75,6 +81,7 @@ export function deviceTrustCryptoServiceFactory(
await devicesApiServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts),
await platformUtilsServiceFactory(cache, opts),
await userDecryptionOptionsServiceFactory(cache, opts),
),
);
}

View File

@@ -9,7 +9,10 @@ import {
ApiServiceInitOptions,
} from "../../../platform/background/service-factories/api-service.factory";
import { appIdServiceFactory } from "../../../platform/background/service-factories/app-id-service.factory";
import { billingAccountProfileStateServiceFactory } from "../../../platform/background/service-factories/billing-account-profile-state-service.factory";
import {
billingAccountProfileStateServiceFactory,
BillingAccountProfileStateServiceInitOptions,
} from "../../../platform/background/service-factories/billing-account-profile-state-service.factory";
import {
CryptoServiceInitOptions,
cryptoServiceFactory,
@@ -70,6 +73,10 @@ import {
} from "./key-connector-service.factory";
import { tokenServiceFactory, TokenServiceInitOptions } from "./token-service.factory";
import { twoFactorServiceFactory, TwoFactorServiceInitOptions } from "./two-factor-service.factory";
import {
internalUserDecryptionOptionServiceFactory,
UserDecryptionOptionsServiceInitOptions,
} from "./user-decryption-options-service.factory";
type LoginStrategyServiceFactoryOptions = FactoryOptions;
@@ -90,7 +97,9 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions
PasswordStrengthServiceInitOptions &
DeviceTrustCryptoServiceInitOptions &
AuthRequestServiceInitOptions &
GlobalStateProviderInitOptions;
UserDecryptionOptionsServiceInitOptions &
GlobalStateProviderInitOptions &
BillingAccountProfileStateServiceInitOptions;
export function loginStrategyServiceFactory(
cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices,
@@ -119,6 +128,7 @@ export function loginStrategyServiceFactory(
await policyServiceFactory(cache, opts),
await deviceTrustCryptoServiceFactory(cache, opts),
await authRequestServiceFactory(cache, opts),
await internalUserDecryptionOptionServiceFactory(cache, opts),
await globalStateProviderFactory(cache, opts),
await billingAccountProfileStateServiceFactory(cache, opts),
),

View File

@@ -0,0 +1,46 @@
import {
InternalUserDecryptionOptionsServiceAbstraction,
UserDecryptionOptionsService,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import {
CachedServices,
factory,
FactoryOptions,
} from "../../../platform/background/service-factories/factory-options";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory";
type UserDecryptionOptionsServiceFactoryOptions = FactoryOptions;
export type UserDecryptionOptionsServiceInitOptions = UserDecryptionOptionsServiceFactoryOptions &
StateProviderInitOptions;
export function userDecryptionOptionsServiceFactory(
cache: {
userDecryptionOptionsService?: InternalUserDecryptionOptionsServiceAbstraction;
} & CachedServices,
opts: UserDecryptionOptionsServiceInitOptions,
): Promise<UserDecryptionOptionsServiceAbstraction> {
return factory(
cache,
"userDecryptionOptionsService",
opts,
async () => new UserDecryptionOptionsService(await stateProviderFactory(cache, opts)),
);
}
export async function internalUserDecryptionOptionServiceFactory(
cache: {
userDecryptionOptionsService?: InternalUserDecryptionOptionsServiceAbstraction;
} & CachedServices,
opts: UserDecryptionOptionsServiceInitOptions,
): Promise<InternalUserDecryptionOptionsServiceAbstraction> {
return (await userDecryptionOptionsServiceFactory(
cache,
opts,
)) as InternalUserDecryptionOptionsServiceAbstraction;
}

View File

@@ -32,6 +32,10 @@ import {
} from "../../../platform/background/service-factories/state-service.factory";
import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory";
import {
userDecryptionOptionsServiceFactory,
UserDecryptionOptionsServiceInitOptions,
} from "./user-decryption-options-service.factory";
import {
UserVerificationApiServiceInitOptions,
userVerificationApiServiceFactory,
@@ -44,6 +48,7 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO
CryptoServiceInitOptions &
I18nServiceInitOptions &
UserVerificationApiServiceInitOptions &
UserDecryptionOptionsServiceInitOptions &
PinCryptoServiceInitOptions &
LogServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions &
@@ -63,6 +68,7 @@ export function userVerificationServiceFactory(
await cryptoServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts),
await userVerificationApiServiceFactory(cache, opts),
await userDecryptionOptionsServiceFactory(cache, opts),
await pinCryptoServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts),

View File

@@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
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";
@@ -37,6 +38,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent {
route: ActivatedRoute,
organizationApiService: OrganizationApiServiceAbstraction,
organizationUserService: OrganizationUserService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService,
) {
@@ -55,6 +57,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent {
stateService,
organizationApiService,
organizationUserService,
userDecryptionOptionsService,
ssoLoginService,
dialogService,
);

View File

@@ -3,7 +3,10 @@ import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
@@ -39,6 +42,7 @@ export class SsoComponent extends BaseSsoComponent {
syncService: SyncService,
environmentService: EnvironmentService,
logService: LogService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
configService: ConfigServiceAbstraction,
protected authService: AuthService,
@Inject(WINDOW) private win: Window,
@@ -56,6 +60,7 @@ export class SsoComponent extends BaseSsoComponent {
environmentService,
passwordGenerationService,
logService,
userDecryptionOptionsService,
configService,
);

View File

@@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router";
import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -16,9 +17,10 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
private activatedRoute: ActivatedRoute,
) {
super(twoFactorService, router, i18nService, platformUtilsService, window);
super(twoFactorService, router, i18nService, platformUtilsService, window, environmentService);
}
close() {

View File

@@ -5,7 +5,10 @@ import { filter, first, takeUntil } from "rxjs/operators";
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
@@ -55,6 +58,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
twoFactorService: TwoFactorService,
appIdService: AppIdService,
loginService: LoginService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
configService: ConfigServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction,
private dialogService: DialogService,
@@ -75,6 +79,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
twoFactorService,
appIdService,
loginService,
userDecryptionOptionsService,
ssoLoginService,
configService,
);

View File

@@ -625,6 +625,22 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
* Updates the position of the overlay button.
*/
private updateOverlayButtonPosition() {
// if (!this.overlayButtonElement) {
// this.createAutofillOverlayButton();
// this.updateCustomElementDefaultStyles(this.overlayButtonElement);
// }
//
// if (!this.isOverlayButtonVisible) {
// this.appendOverlayElementToBody(this.overlayButtonElement);
// this.isOverlayButtonVisible = true;
// this.setOverlayRepositionEventListeners();
// }
// // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// // eslint-disable-next-line @typescript-eslint/no-floating-promises
// this.sendExtensionMessage("updateAutofillOverlayPosition", {
// overlayElement: AutofillOverlayElement.Button,
// });
void this.sendExtensionMessage("updateAutofillOverlayPosition", {
overlayElement: AutofillOverlayElement.Button,
});
@@ -634,6 +650,22 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
* Updates the position of the overlay list.
*/
private updateOverlayListPosition() {
// if (!this.overlayListElement) {
// this.createAutofillOverlayList();
// this.updateCustomElementDefaultStyles(this.overlayListElement);
// }
//
// if (!this.isOverlayListVisible) {
// this.appendOverlayElementToBody(this.overlayListElement);
// this.isOverlayListVisible = true;
// }
//
// // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// // eslint-disable-next-line @typescript-eslint/no-floating-promises
// this.sendExtensionMessage("updateAutofillOverlayPosition", {
// overlayElement: AutofillOverlayElement.List,
// });
void this.sendExtensionMessage("updateAutofillOverlayPosition", {
overlayElement: AutofillOverlayElement.List,
});

View File

@@ -5,6 +5,8 @@ import {
PinCryptoService,
LoginStrategyServiceAbstraction,
LoginStrategyService,
InternalUserDecryptionOptionsServiceAbstraction,
UserDecryptionOptionsService,
AuthRequestServiceAbstraction,
AuthRequestService,
} from "@bitwarden/auth/common";
@@ -242,6 +244,7 @@ export default class MainBackground {
environmentService: BrowserEnvironmentService;
cipherService: CipherServiceAbstraction;
folderService: InternalFolderServiceAbstraction;
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction;
collectionService: CollectionServiceAbstraction;
vaultTimeoutService: VaultTimeoutService;
vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction;
@@ -539,6 +542,8 @@ export default class MainBackground {
};
})();
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
this.deviceTrustCryptoService = new DeviceTrustCryptoService(
this.keyGenerationService,
@@ -550,6 +555,7 @@ export default class MainBackground {
this.devicesApiService,
this.i18nService,
this.platformUtilsService,
this.userDecryptionOptionsService,
);
this.devicesService = new DevicesServiceImplementation(this.devicesApiService);
@@ -590,6 +596,7 @@ export default class MainBackground {
this.policyService,
this.deviceTrustCryptoService,
this.authRequestService,
this.userDecryptionOptionsService,
this.globalStateProvider,
this.billingAccountProfileStateService,
);
@@ -631,6 +638,7 @@ export default class MainBackground {
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.userDecryptionOptionsService,
this.cryptoService,
this.tokenService,
this.policyService,
@@ -650,6 +658,7 @@ export default class MainBackground {
this.cryptoService,
this.i18nService,
this.userVerificationApiService,
this.userDecryptionOptionsService,
this.pinCryptoService,
this.logService,
this.vaultTimeoutSettingsService,
@@ -717,6 +726,7 @@ export default class MainBackground {
this.folderApiService,
this.organizationService,
this.sendApiService,
this.userDecryptionOptionsService,
this.avatarService,
logoutCallback,
this.billingAccountProfileStateService,

View File

@@ -9,6 +9,10 @@ import {
tokenServiceFactory,
TokenServiceInitOptions,
} from "../../auth/background/service-factories/token-service.factory";
import {
userDecryptionOptionsServiceFactory,
UserDecryptionOptionsServiceInitOptions,
} from "../../auth/background/service-factories/user-decryption-options-service.factory";
import {
biometricStateServiceFactory,
BiometricStateServiceInitOptions,
@@ -30,6 +34,7 @@ import {
type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions;
export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsServiceFactoryOptions &
UserDecryptionOptionsServiceInitOptions &
CryptoServiceInitOptions &
TokenServiceInitOptions &
PolicyServiceInitOptions &
@@ -46,6 +51,7 @@ export function vaultTimeoutSettingsServiceFactory(
opts,
async () =>
new VaultTimeoutSettingsService(
await userDecryptionOptionsServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts),
await tokenServiceFactory(cache, opts),
await policyServiceFactory(cache, opts),

View File

@@ -16,6 +16,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
import { AvatarModule, ButtonModule } from "@bitwarden/components";
import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui";
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
import { AccountComponent } from "../auth/popup/account-switching/account.component";
@@ -107,6 +108,7 @@ import "../platform/popup/locales";
AvatarModule,
AccountComponent,
ButtonModule,
ExportScopeCalloutComponent,
],
declarations: [
ActionButtonsComponent,

View File

@@ -82,7 +82,6 @@ import {
import { SearchService } from "@bitwarden/common/services/search.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import {
@@ -96,7 +95,6 @@ import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vau
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { DialogService } from "@bitwarden/components";
import { ImportServiceAbstraction } from "@bitwarden/importer/core";
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
import { UnauthGuardService } from "../../auth/popup/services";
@@ -292,11 +290,6 @@ function getBgService<T>(service: keyof MainBackground) {
},
deps: [DomSanitizer, ToastrService],
},
{
provide: PasswordStrengthServiceAbstraction,
useFactory: getBgService<PasswordStrengthServiceAbstraction>("passwordStrengthService"),
deps: [],
},
{
provide: PasswordGenerationServiceAbstraction,
useFactory: getBgService<PasswordGenerationServiceAbstraction>("passwordGenerationService"),
@@ -350,11 +343,6 @@ function getBgService<T>(service: keyof MainBackground) {
useFactory: getBgService<AutofillService>("autofillService"),
deps: [],
},
{
provide: ImportServiceAbstraction,
useFactory: getBgService<ImportServiceAbstraction>("importService"),
deps: [],
},
{
provide: VaultExportServiceAbstraction,
useFactory: getBgService<VaultExportServiceAbstraction>("exportService"),

View File

@@ -19,7 +19,7 @@
<app-callout type="warning" title="{{ 'vaultExportDisabled' | i18n }}" *ngIf="disabledByPolicy">
{{ "personalVaultExportPolicyInEffect" | i18n }}
</app-callout>
<app-export-scope-callout *ngIf="!disabledByPolicy"></app-export-scope-callout>
<tools-export-scope-callout *ngIf="!disabledByPolicy"></tools-export-scope-callout>
<div class="box">
<div class="box-content">

View File

@@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
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";
@@ -21,11 +22,19 @@ export class CollectionsComponent extends BaseCollectionsComponent {
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
cipherService: CipherService,
organizationService: OrganizationService,
private route: ActivatedRoute,
private location: Location,
logService: LogService,
) {
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
organizationService,
logService,
);
}
async ngOnInit() {

View File

@@ -20,6 +20,7 @@
"@bitwarden/vault-export-core": [
"../../libs/tools/export/vault-export/vault-export-core/src"
],
"@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"],
"@bitwarden/importer/core": ["../../libs/importer/src"],
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/platform": ["../../libs/platform/src"],

View File

@@ -5,11 +5,13 @@ import { program } from "commander";
import * as jsdom from "jsdom";
import {
InternalUserDecryptionOptionsServiceAbstraction,
AuthRequestService,
LoginStrategyService,
LoginStrategyServiceAbstraction,
PinCryptoService,
PinCryptoServiceAbstraction,
UserDecryptionOptionsService,
} from "@bitwarden/auth/common";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
@@ -169,6 +171,7 @@ export class Main {
eventUploadService: EventUploadServiceAbstraction;
passwordGenerationService: PasswordGenerationServiceAbstraction;
passwordStrengthService: PasswordStrengthServiceAbstraction;
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction;
totpService: TotpService;
containerService: ContainerService;
auditService: AuditService;
@@ -436,6 +439,8 @@ export class Main {
this.stateService,
);
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
this.deviceTrustCryptoService = new DeviceTrustCryptoService(
this.keyGenerationService,
@@ -447,6 +452,7 @@ export class Main {
this.devicesApiService,
this.i18nService,
this.platformUtilsService,
this.userDecryptionOptionsService,
);
this.authRequestService = new AuthRequestService(
@@ -478,6 +484,7 @@ export class Main {
this.policyService,
this.deviceTrustCryptoService,
this.authRequestService,
this.userDecryptionOptionsService,
this.globalStateProvider,
this.billingAccountProfileStateService,
);
@@ -529,6 +536,7 @@ export class Main {
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.userDecryptionOptionsService,
this.cryptoService,
this.tokenService,
this.policyService,
@@ -548,6 +556,7 @@ export class Main {
this.cryptoService,
this.i18nService,
this.userVerificationApiService,
this.userDecryptionOptionsService,
this.pinCryptoService,
this.logService,
this.vaultTimeoutSettingsService,
@@ -589,6 +598,7 @@ export class Main {
this.folderApiService,
this.organizationService,
this.sendApiService,
this.userDecryptionOptionsService,
this.avatarService,
async (expired: boolean) => await this.logout(),
this.billingAccountProfileStateService,

View File

@@ -84,9 +84,9 @@ dependencies = [
[[package]]
name = "base64"
version = "0.21.5"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
[[package]]
name = "bitflags"

View File

@@ -16,7 +16,7 @@ manual_test = []
aes = "=0.8.4"
anyhow = "=1.0.80"
arboard = { version = "=3.3.0", default-features = false, features = ["wayland-data-control"] }
base64 = "=0.21.5"
base64 = "=0.22.0"
cbc = { version = "=0.1.2", features = ["alloc"] }
napi = { version = "=2.13.3", features = ["async"] }
napi-derive = "=2.13.0"

View File

@@ -412,6 +412,23 @@
"enableBrowserIntegrationFingerprintDesc" | i18n
}}</small>
</div>
<div class="form-group">
<div class="checkbox">
<label for="enableHardwareAcceleration">
<input
id="enableHardwareAcceleration"
type="checkbox"
aria-describedby="enableHardwareAccelerationHelp"
formControlName="enableHardwareAcceleration"
(change)="saveHardwareAcceleration()"
/>
{{ "enableHardwareAcceleration" | i18n }}
</label>
</div>
<small id="enableHardwareAccelerationHelp" class="help-block">{{
"enableHardwareAccelerationDesc" | i18n
}}</small>
</div>
<div class="form-group" *ngIf="showDuckDuckGoIntegrationOption">
<div class="checkbox">
<label for="enableDuckDuckGoBrowserIntegration">

View File

@@ -24,6 +24,7 @@ import { DialogService } from "@bitwarden/components";
import { SetPinComponent } from "../../auth/components/set-pin.component";
import { flagEnabled } from "../../platform/flags";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
@Component({
selector: "app-settings",
@@ -96,6 +97,7 @@ export class SettingsComponent implements OnInit {
value: false,
disabled: true,
}),
enableHardwareAcceleration: true,
enableDuckDuckGoBrowserIntegration: false,
theme: [null as ThemeType | null],
locale: [null as string | null],
@@ -119,6 +121,7 @@ export class SettingsComponent implements OnInit {
private dialogService: DialogService,
private userVerificationService: UserVerificationServiceAbstraction,
private biometricStateService: BiometricStateService,
private desktopSettingsService: DesktopSettingsService,
) {
const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
@@ -259,6 +262,9 @@ export class SettingsComponent implements OnInit {
enableBrowserIntegration: await this.stateService.getEnableBrowserIntegration(),
enableBrowserIntegrationFingerprint:
await this.stateService.getEnableBrowserIntegrationFingerprint(),
enableHardwareAcceleration: await firstValueFrom(
this.desktopSettingsService.hardwareAcceleration$,
),
enableDuckDuckGoBrowserIntegration:
await this.stateService.getEnableDuckDuckGoBrowserIntegration(),
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
@@ -655,6 +661,12 @@ export class SettingsComponent implements OnInit {
);
}
async saveHardwareAcceleration() {
await this.desktopSettingsService.setHardwareAcceleration(
this.form.value.enableHardwareAcceleration,
);
}
async updateApproveLoginRequests() {
await this.stateService.setApproveLoginRequests(this.form.value.approveLoginRequests);
}

View File

@@ -8,6 +8,7 @@ import { NgModule } from "@angular/core";
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
import { DialogModule } from "@bitwarden/components";
import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui";
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
import { DeleteAccountComponent } from "../auth/delete-account.component";
@@ -61,6 +62,7 @@ import { SendComponent } from "./tools/send/send.component";
DialogModule,
DeleteAccountComponent,
UserVerificationComponent,
ExportScopeCalloutComponent,
],
declarations: [
AccessibilityCookieComponent,

View File

@@ -54,6 +54,7 @@ import { DialogService } from "@bitwarden/components";
import { LoginGuard } from "../../auth/guards/login.guard";
import { Account } from "../../models/account";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
import { ElectronCryptoService } from "../../platform/services/electron-crypto.service";
import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service";
import {
@@ -212,6 +213,11 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
BiometricStateService,
],
},
{
provide: DesktopSettingsService,
useClass: DesktopSettingsService,
deps: [StateProvider],
},
],
})
export class ServicesModule {}

View File

@@ -9,7 +9,7 @@
>
{{ "personalVaultExportPolicyInEffect" | i18n }}
</app-callout>
<app-export-scope-callout *ngIf="!disabledByPolicy"></app-export-scope-callout>
<tools-export-scope-callout *ngIf="!disabledByPolicy"></tools-export-scope-callout>
<div class="box">
<h1 class="box-header" id="exportTitle">
{{ "exportVault" | i18n }}

View File

@@ -2,6 +2,7 @@ import { Component, NgZone, OnDestroy } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
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";
@@ -44,6 +45,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
stateService: StateService,
organizationApiService: OrganizationApiServiceAbstraction,
organizationUserService: OrganizationUserService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService,
) {
@@ -62,6 +64,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
stateService,
organizationApiService,
organizationUserService,
userDecryptionOptionsService,
ssoLoginService,
dialogService,
);

View File

@@ -2,7 +2,10 @@ import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
@@ -34,6 +37,7 @@ export class SsoComponent extends BaseSsoComponent {
environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
logService: LogService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
configService: ConfigServiceAbstraction,
) {
super(
@@ -49,6 +53,7 @@ export class SsoComponent extends BaseSsoComponent {
environmentService,
passwordGenerationService,
logService,
userDecryptionOptionsService,
configService,
);
super.onSuccessfulLogin = async () => {

View File

@@ -3,6 +3,7 @@ import { Router } from "@angular/router";
import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -16,7 +17,8 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
) {
super(twoFactorService, router, i18nService, platformUtilsService, window);
super(twoFactorService, router, i18nService, platformUtilsService, window, environmentService);
}
}

View File

@@ -4,7 +4,10 @@ import { ActivatedRoute, Router } from "@angular/router";
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
@@ -53,6 +56,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
twoFactorService: TwoFactorService,
appIdService: AppIdService,
loginService: LoginService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction,
configService: ConfigServiceAbstraction,
@Inject(WINDOW) protected win: Window,
@@ -71,6 +75,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
twoFactorService,
appIdService,
loginService,
userDecryptionOptionsService,
ssoLoginService,
configService,
);

View File

@@ -1644,6 +1644,12 @@
"enableBrowserIntegrationFingerprintDesc": {
"message": "Add an additional layer of security by requiring fingerprint phrase confirmation when establishing a link between your desktop and browser. This requires user action and verification each time a connection is created."
},
"enableHardwareAcceleration": {
"message": "Use hardware acceleration"
},
"enableHardwareAccelerationDesc": {
"message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required."
},
"approve": {
"message": "Approve"
},

View File

@@ -1,6 +1,7 @@
import * as path from "path";
import { app } from "electron";
import { firstValueFrom } from "rxjs";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
@@ -37,6 +38,7 @@ import { BiometricsService, BiometricsServiceAbstraction } from "./platform/main
import { ClipboardMain } from "./platform/main/clipboard.main";
import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener";
import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service";
import { DesktopSettingsService } from "./platform/services/desktop-settings.service";
import { ElectronLogMainService } from "./platform/services/electron-log.main.service";
import { ELECTRON_SUPPORTS_SECURE_STORAGE } from "./platform/services/electron-platform-utils.service";
import { ElectronStateService } from "./platform/services/electron-state.service";
@@ -56,6 +58,7 @@ export class Main {
mainCryptoFunctionService: MainCryptoFunctionService;
desktopCredentialStorageListener: DesktopCredentialStorageListener;
migrationRunner: MigrationRunner;
desktopSettingsService: DesktopSettingsService;
tokenService: TokenServiceAbstraction;
windowMain: WindowMain;
@@ -189,6 +192,7 @@ export class Main {
this.messagingMain = new MessagingMain(this, this.stateService);
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain);
this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.stateService);
this.desktopSettingsService = new DesktopSettingsService(stateProvider);
this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => {
this.messagingMain.onMessage(message);
@@ -237,6 +241,7 @@ export class Main {
// Run migrations first, then other things
this.migrationRunner.run().then(
async () => {
await this.toggleHardwareAcceleration();
await this.windowMain.init();
await this.i18nService.init();
this.messagingMain.init();
@@ -307,4 +312,15 @@ export class Main {
this.messagingService.send("deepLink", { urlString: s });
});
}
private async toggleHardwareAcceleration(): Promise<void> {
const hardwareAcceleration = await firstValueFrom(
this.desktopSettingsService.hardwareAcceleration$,
);
if (!hardwareAcceleration) {
this.logService.warning("Hardware acceleration is disabled");
app.disableHardwareAcceleration();
}
}
}

View File

@@ -0,0 +1,26 @@
import { map } from "rxjs";
import {
DESKTOP_SETTINGS_DISK,
KeyDefinition,
StateProvider,
} from "@bitwarden/common/platform/state";
export const HARDWARE_ACCELERATION = new KeyDefinition<boolean>(
DESKTOP_SETTINGS_DISK,
"hardwareAcceleration",
{
deserializer: (v: boolean) => v,
},
);
export class DesktopSettingsService {
private hwState = this.stateProvider.getGlobal(HARDWARE_ACCELERATION);
hardwareAcceleration$ = this.hwState.state$.pipe(map((v) => v ?? true));
constructor(private stateProvider: StateProvider) {}
async setHardwareAcceleration(enabled: boolean) {
await this.hwState.update(() => enabled);
}
}

View File

@@ -1,6 +1,7 @@
import { Component } from "@angular/core";
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
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";
@@ -17,8 +18,16 @@ export class CollectionsComponent extends BaseCollectionsComponent {
i18nService: I18nService,
collectionService: CollectionService,
platformUtilsService: PlatformUtilsService,
organizationService: OrganizationService,
logService: LogService,
) {
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
organizationService,
logService,
);
}
}

View File

@@ -20,6 +20,7 @@
"@bitwarden/vault-export-core": [
"../../libs/tools/export/vault-export/vault-export-core/src"
],
"@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"],
"@bitwarden/importer/core": ["../../libs/importer/src"],
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/node/*": ["../../libs/node/src/*"],

View File

@@ -1,10 +1,12 @@
import { Directive, ViewChild, ViewContainerRef } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import {
OrganizationUserStatusType,
OrganizationUserType,
@@ -17,7 +19,6 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DialogService } from "@bitwarden/components";
@@ -109,8 +110,8 @@ export abstract class BasePeopleComponent<
private logService: LogService,
private searchPipe: SearchPipe,
protected userNamePipe: UserNamePipe,
protected stateService: StateService,
protected dialogService: DialogService,
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
) {}
abstract edit(user: UserType): void;
@@ -351,7 +352,9 @@ export abstract class BasePeopleComponent<
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const autoConfirm = await this.stateService.getAutoConfirmFingerPrints();
const autoConfirm = await firstValueFrom(
this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
);
if (autoConfirm == null || !autoConfirm) {
const [modal] = await this.modalService.openViewRef(
UserConfirmComponent,

View File

@@ -1,8 +1,8 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@Component({
selector: "app-user-confirm",
@@ -22,7 +22,7 @@ export class UserConfirmComponent implements OnInit {
constructor(
private cryptoService: CryptoService,
private logService: LogService,
private stateService: StateService,
private organizationManagementPreferencesService: OrganizationManagementPreferencesService,
) {}
async ngOnInit() {
@@ -45,7 +45,7 @@ export class UserConfirmComponent implements OnInit {
}
if (this.dontAskAgain) {
await this.stateService.setAutoConfirmFingerprints(true);
await this.organizationManagementPreferencesService.autoConfirmFingerPrints.set(true);
}
this.onConfirmedUser.emit();

View File

@@ -21,6 +21,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserConfirmRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import {
@@ -43,7 +44,6 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@@ -117,7 +117,6 @@ export class PeopleComponent
searchPipe: SearchPipe,
userNamePipe: UserNamePipe,
private syncService: SyncService,
stateService: StateService,
private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService,
@@ -125,6 +124,7 @@ export class PeopleComponent
private router: Router,
private groupService: GroupService,
private collectionService: CollectionService,
organizationManagementPreferencesService: OrganizationManagementPreferencesService,
) {
super(
apiService,
@@ -137,8 +137,8 @@ export class PeopleComponent
logService,
searchPipe,
userNamePipe,
stateService,
dialogService,
organizationManagementPreferencesService,
);
}

View File

@@ -1,12 +1,19 @@
import { NgModule } from "@angular/core";
import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui";
import { LooseComponentsModule, SharedModule } from "../../../../shared";
import { OrganizationVaultExportRoutingModule } from "./org-vault-export-routing.module";
import { OrganizationVaultExportComponent } from "./org-vault-export.component";
@NgModule({
imports: [SharedModule, LooseComponentsModule, OrganizationVaultExportRoutingModule],
imports: [
SharedModule,
LooseComponentsModule,
OrganizationVaultExportRoutingModule,
ExportScopeCalloutComponent,
],
declarations: [OrganizationVaultExportComponent],
})
export class OrganizationVaultExportModule {}

View File

@@ -3,9 +3,9 @@ import { Component, OnInit, Inject } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DialogService } from "@bitwarden/components";
@@ -36,7 +36,7 @@ export class EmergencyAccessConfirmComponent implements OnInit {
private formBuilder: FormBuilder,
private apiService: ApiService,
private cryptoService: CryptoService,
private stateService: StateService,
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
private logService: LogService,
private dialogRef: DialogRef<EmergencyAccessConfirmDialogResult>,
) {}
@@ -63,7 +63,7 @@ export class EmergencyAccessConfirmComponent implements OnInit {
}
if (this.confirmForm.get("dontAskAgain").value) {
await this.stateService.setAutoConfirmFingerprints(true);
await this.organizationManagementPreferencesService.autoConfirmFingerPrints.set(true);
}
try {

View File

@@ -3,6 +3,7 @@ import { lastValueFrom, Observable, firstValueFrom } from "rxjs";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -64,6 +65,7 @@ export class EmergencyAccessComponent implements OnInit {
private organizationService: OrganizationService,
protected dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
) {
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
}
@@ -136,7 +138,9 @@ export class EmergencyAccessComponent implements OnInit {
return;
}
const autoConfirm = await this.stateService.getAutoConfirmFingerPrints();
const autoConfirm = await firstValueFrom(
this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
);
if (autoConfirm == null || !autoConfirm) {
const dialogRef = EmergencyAccessConfirmComponent.open(this.dialogService, {
data: {

View File

@@ -3,7 +3,10 @@ import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction";
import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response";
@@ -41,6 +44,7 @@ export class SsoComponent extends BaseSsoComponent {
logService: LogService,
private orgDomainApiService: OrgDomainApiServiceAbstraction,
private validationService: ValidationService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
configService: ConfigServiceAbstraction,
) {
super(
@@ -56,6 +60,7 @@ export class SsoComponent extends BaseSsoComponent {
environmentService,
passwordGenerationService,
logService,
userDecryptionOptionsService,
configService,
);
this.redirectUri = window.location.origin + "/sso-connector.html";

View File

@@ -3,6 +3,7 @@ import { Router } from "@angular/router";
import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -16,7 +17,8 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
) {
super(twoFactorService, router, i18nService, platformUtilsService, window);
super(twoFactorService, router, i18nService, platformUtilsService, window, environmentService);
}
}

View File

@@ -4,7 +4,10 @@ import { ActivatedRoute, Router } from "@angular/router";
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
@@ -44,6 +47,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
twoFactorService: TwoFactorService,
appIdService: AppIdService,
loginService: LoginService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction,
configService: ConfigServiceAbstraction,
@Inject(WINDOW) protected win: Window,
@@ -62,6 +66,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
twoFactorService,
appIdService,
loginService,
userDecryptionOptionsService,
ssoLoginService,
configService,
);

View File

@@ -5,10 +5,10 @@
<bit-callout type="danger" title="{{ 'vaultExportDisabled' | i18n }}" *ngIf="disabledByPolicy">
{{ "personalVaultExportPolicyInEffect" | i18n }}
</bit-callout>
<app-export-scope-callout
<tools-export-scope-callout
[organizationId]="organizationId"
*ngIf="!disabledByPolicy"
></app-export-scope-callout>
></tools-export-scope-callout>
<ng-container *ngIf="organizations$ | async as organizations">
<bit-form-field *ngIf="organizations.length > 0">

View File

@@ -1,12 +1,14 @@
import { NgModule } from "@angular/core";
import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui";
import { LooseComponentsModule, SharedModule } from "../../shared";
import { ExportRoutingModule } from "./export-routing.module";
import { ExportComponent } from "./export.component";
@NgModule({
imports: [SharedModule, LooseComponentsModule, ExportRoutingModule],
imports: [SharedModule, LooseComponentsModule, ExportRoutingModule, ExportScopeCalloutComponent],
declarations: [ExportComponent],
})
export class ExportModule {}

View File

@@ -40,6 +40,7 @@
[(ngModel)]="$any(c).checked"
name="Collection[{{ i }}].Checked"
appStopProp
[disabled]="!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)"
/>
</td>
<td>

View File

@@ -1,6 +1,7 @@
import { Component, OnDestroy } from "@angular/core";
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
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";
@@ -18,9 +19,17 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
cipherService: CipherService,
organizationSerivce: OrganizationService,
logService: LogService,
) {
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
organizationSerivce,
logService,
);
}
ngOnDestroy() {
@@ -28,6 +37,9 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
}
check(c: CollectionView, select?: boolean) {
if (!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)) {
return;
}
(c as any).checked = select == null ? !(c as any).checked : select;
}

View File

@@ -1,6 +1,7 @@
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
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";
@@ -12,7 +13,6 @@ 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";
@@ -44,8 +44,8 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
private logService: LogService,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private dialogService: DialogService,
private stateService: StateService,
) {}
async ngOnInit() {
@@ -56,7 +56,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
combineLatest([
this.organization$,
resetPasswordPolicies$,
this.stateService.getAccountDecryptionOptions(),
this.userDecryptionOptionsService.userDecryptionOptions$,
])
.pipe(takeUntil(this.destroy$))
.subscribe(([organization, resetPasswordPolicies, decryptionOptions]) => {

View File

@@ -1,6 +1,7 @@
import { Component } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -25,15 +26,27 @@ export class CollectionsComponent extends BaseCollectionsComponent {
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
cipherService: CipherService,
organizationService: OrganizationService,
private apiService: ApiService,
logService: LogService,
) {
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
organizationService,
logService,
);
this.allowSelectNone = true;
}
protected async loadCipher() {
if (!this.organization.canViewAllCollections) {
// if cipher is unassigned use apiService. We can see this by looking at this.collectionIds
if (
!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
this.collectionIds.length !== 0
) {
return await super.loadCipher();
}
const response = await this.apiService.getCipherAdmin(this.cipherId);
@@ -55,7 +68,10 @@ export class CollectionsComponent extends BaseCollectionsComponent {
}
protected saveCollections() {
if (this.organization.canEditAnyCollection) {
if (
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) ||
this.collectionIds.length === 0
) {
const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds);
return this.apiService.putCipherCollectionsAdmin(this.cipherId, request);
} else {

View File

@@ -137,6 +137,7 @@ export class VaultComponent implements OnInit, OnDestroy {
protected showCollectionAccessRestricted: boolean;
protected currentSearchText$: Observable<string>;
protected editableCollections$: Observable<CollectionView[]>;
protected allCollectionsWithoutUnassigned$: Observable<CollectionAdminView[]>;
protected showBulkEditCollectionAccess$ = this.configService.getFeatureFlag$(
FeatureFlag.BulkCollectionAccess,
false,
@@ -253,7 +254,7 @@ export class VaultComponent implements OnInit, OnDestroy {
this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search));
const allCollectionsWithoutUnassigned$ = combineLatest([
this.allCollectionsWithoutUnassigned$ = combineLatest([
organizationId$.pipe(switchMap((orgId) => this.collectionAdminService.getAll(orgId))),
defer(() => this.collectionService.getAllDecrypted()),
]).pipe(
@@ -276,7 +277,7 @@ export class VaultComponent implements OnInit, OnDestroy {
shareReplay({ refCount: true, bufferSize: 1 }),
);
this.editableCollections$ = allCollectionsWithoutUnassigned$.pipe(
this.editableCollections$ = this.allCollectionsWithoutUnassigned$.pipe(
map((collections) => {
// Users that can edit all ciphers can implicitly edit all collections
if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
@@ -287,7 +288,10 @@ export class VaultComponent implements OnInit, OnDestroy {
shareReplay({ refCount: true, bufferSize: 1 }),
);
const allCollections$ = combineLatest([organizationId$, allCollectionsWithoutUnassigned$]).pipe(
const allCollections$ = combineLatest([
organizationId$,
this.allCollectionsWithoutUnassigned$,
]).pipe(
map(([organizationId, allCollections]) => {
const noneCollection = new CollectionAdminView();
noneCollection.name = this.i18nService.t("unassigned");
@@ -680,16 +684,35 @@ export class VaultComponent implements OnInit, OnDestroy {
if (this.flexibleCollectionsV1Enabled) {
// V1 limits admins to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
} else {
collections = (await firstValueFrom(this.vaultFilterService.filteredCollections$)).filter(
(c) => !c.readOnly && c.id != Unassigned,
collections = await firstValueFrom(
this.allCollectionsWithoutUnassigned$.pipe(
map((c) => {
return c.sort((a, b) => {
if (
a.canEditItems(this.organization, true) &&
!b.canEditItems(this.organization, true)
) {
return -1;
} else if (
!a.canEditItems(this.organization, true) &&
b.canEditItems(this.organization, true)
) {
return 1;
} else {
return a.name.localeCompare(b.name);
}
});
}),
),
);
} else {
collections = await firstValueFrom(this.allCollectionsWithoutUnassigned$);
}
const [modal] = await this.modalService.openViewRef(
CollectionsComponent,
this.collectionsModalRef,
(comp) => {
comp.flexibleCollectionsV1Enabled = this.flexibleCollectionsV1Enabled;
comp.collectionIds = cipher.collectionIds;
comp.collections = collections;
comp.organization = this.organization;

View File

@@ -15,6 +15,7 @@
"@bitwarden/vault-export-core": [
"../../libs/tools/export/vault-export/vault-export-core/src"
],
"@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"],
"@bitwarden/importer/core": ["../../libs/importer/src"],
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/platform": ["../../libs/platform/src"],

View File

@@ -7,6 +7,7 @@ import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderUserStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums";
import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request";
@@ -18,7 +19,6 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { DialogService } from "@bitwarden/components";
import { BasePeopleComponent } from "@bitwarden/web-vault/app/admin-console/common/base.people.component";
@@ -67,9 +67,9 @@ export class PeopleComponent
logService: LogService,
searchPipe: SearchPipe,
userNamePipe: UserNamePipe,
stateService: StateService,
private providerService: ProviderService,
dialogService: DialogService,
organizationManagementPreferencesService: OrganizationManagementPreferencesService,
) {
super(
apiService,
@@ -82,8 +82,8 @@ export class PeopleComponent
logService,
searchPipe,
userNamePipe,
stateService,
dialogService,
organizationManagementPreferencesService,
);
}

View File

@@ -13,6 +13,7 @@ const routes: Routes = [
},
{
path: "sm",
canActivate: [deepLinkGuard()],
loadChildren: async () =>
(await import("./secrets-manager/secrets-manager.module")).SecretsManagerModule,
},

View File

@@ -11,6 +11,7 @@
"@bitwarden/vault-export-core": [
"../../libs/tools/export/vault-export/vault-export-core/src"
],
"@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-core/src"],
"@bitwarden/platform": ["../../libs/platform/src"],
"@bitwarden/vault": ["../../libs/vault/src"],
"@bitwarden/web-vault/*": ["../../apps/web/src/*"]

View File

@@ -1,5 +1,7 @@
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
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";
@@ -19,6 +21,8 @@ export class CollectionsComponent implements OnInit {
cipher: CipherView;
collectionIds: string[];
collections: CollectionView[] = [];
organization: Organization;
flexibleCollectionsV1Enabled: boolean;
protected cipherDomain: Cipher;
@@ -27,6 +31,7 @@ export class CollectionsComponent implements OnInit {
protected platformUtilsService: PlatformUtilsService,
protected i18nService: I18nService,
protected cipherService: CipherService,
protected organizationService: OrganizationService,
private logService: LogService,
) {}
@@ -48,11 +53,21 @@ export class CollectionsComponent implements OnInit {
(c as any).checked = this.collectionIds != null && this.collectionIds.indexOf(c.id) > -1;
});
}
if (this.organization == null) {
this.organization = await this.organizationService.get(this.cipher.organizationId);
}
}
async submit() {
const selectedCollectionIds = this.collections
.filter((c) => !!(c as any).checked)
.filter((c) => {
if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
return !!(c as any).checked;
} else {
return !!(c as any).checked && c.readOnly == null;
}
})
.map((c) => c.id);
if (!this.allowSelectNone && selectedCollectionIds.length === 0) {
this.platformUtilsService.showToast(

View File

@@ -14,6 +14,10 @@ import {
throwError,
} from "rxjs";
import {
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
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";
@@ -30,7 +34,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
enum State {
NewUser,
@@ -88,6 +91,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
protected validationService: ValidationService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected platformUtilsService: PlatformUtilsService,
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction,
) {}
@@ -101,14 +105,15 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
await this.setRememberDeviceDefaultValue();
try {
const accountDecryptionOptions: AccountDecryptionOptions =
await this.stateService.getAccountDecryptionOptions();
const userDecryptionOptions = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
);
// see sso-login.strategy - to determine if a user is new or not it just checks if there is a key on the token response..
// can we check if they have a user key or master key in crypto service? Would that be sufficient?
if (
!accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval &&
!accountDecryptionOptions?.hasMasterPassword
!userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval &&
!userDecryptionOptions?.hasMasterPassword
) {
// We are dealing with a new account if:
// - User does not have admin approval (i.e. has not enrolled into admin reset)
@@ -118,7 +123,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.loadNewUserData();
} else {
this.loadUntrustedDeviceData(accountDecryptionOptions);
this.loadUntrustedDeviceData(userDecryptionOptions);
}
// Note: this is probably not a comprehensive write up of all scenarios:
@@ -195,7 +200,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
this.loading = false;
}
loadUntrustedDeviceData(accountDecryptionOptions: AccountDecryptionOptions) {
loadUntrustedDeviceData(userDecryptionOptions: UserDecryptionOptions) {
this.loading = true;
const email$ = from(this.stateService.getEmail()).pipe(
@@ -215,13 +220,12 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
)
.subscribe((email) => {
const showApproveFromOtherDeviceBtn =
accountDecryptionOptions?.trustedDeviceOption?.hasLoginApprovingDevice || false;
userDecryptionOptions?.trustedDeviceOption?.hasLoginApprovingDevice || false;
const showReqAdminApprovalBtn =
!!accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false;
!!userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false;
const showApproveWithMasterPasswordBtn =
accountDecryptionOptions?.hasMasterPassword || false;
const showApproveWithMasterPasswordBtn = userDecryptionOptions?.hasMasterPassword || false;
const userEmail = email;

View File

@@ -1,8 +1,9 @@
import { Directive } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { of } from "rxjs";
import { firstValueFrom, of } from "rxjs";
import { filter, first, switchMap, tap } from "rxjs/operators";
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
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";
@@ -26,7 +27,6 @@ import {
DEFAULT_KDF_CONFIG,
} from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
@@ -64,6 +64,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
stateService: StateService,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService,
private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService,
) {
@@ -228,11 +229,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None);
// User now has a password so update account decryption options in state
const acctDecryptionOpts: AccountDecryptionOptions =
await this.stateService.getAccountDecryptionOptions();
acctDecryptionOpts.hasMasterPassword = true;
await this.stateService.setAccountDecryptionOptions(acctDecryptionOpts);
const userDecryptionOpts = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
);
userDecryptionOpts.hasMasterPassword = true;
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
await this.stateService.setKdfType(this.kdf);
await this.stateService.setKdfConfig(this.kdfConfig);

View File

@@ -2,16 +2,20 @@ import { Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute, Router } from "@angular/router";
import { MockProxy, mock } from "jest-mock-extended";
import { Observable, of } from "rxjs";
import { BehaviorSubject, Observable, of } from "rxjs";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import {
FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption,
LoginStrategyServiceAbstraction,
FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption,
FakeUserDecryptionOptions as UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { KeyConnectorUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@@ -19,7 +23,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
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 { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { SsoComponent } from "./sso.component";
@@ -62,6 +65,7 @@ describe("SsoComponent", () => {
let mockEnvironmentService: MockProxy<EnvironmentService>;
let mockPasswordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
let mockLogService: MockProxy<LogService>;
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
// Mock authService.logIn params
@@ -77,17 +81,19 @@ describe("SsoComponent", () => {
let mockOnSuccessfulLoginForceResetNavigate: jest.Mock;
let mockOnSuccessfulLoginTdeNavigate: jest.Mock;
let mockAcctDecryptionOpts: {
noMasterPassword: AccountDecryptionOptions;
withMasterPassword: AccountDecryptionOptions;
withMasterPasswordAndTrustedDevice: AccountDecryptionOptions;
withMasterPasswordAndTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
withMasterPasswordAndKeyConnector: AccountDecryptionOptions;
noMasterPasswordWithTrustedDevice: AccountDecryptionOptions;
noMasterPasswordWithTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
noMasterPasswordWithKeyConnector: AccountDecryptionOptions;
let mockUserDecryptionOpts: {
noMasterPassword: UserDecryptionOptions;
withMasterPassword: UserDecryptionOptions;
withMasterPasswordAndTrustedDevice: UserDecryptionOptions;
withMasterPasswordAndTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
withMasterPasswordAndKeyConnector: UserDecryptionOptions;
noMasterPasswordWithTrustedDevice: UserDecryptionOptions;
noMasterPasswordWithTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
noMasterPasswordWithKeyConnector: UserDecryptionOptions;
};
let selectedUserDecryptionOptions: BehaviorSubject<UserDecryptionOptions>;
beforeEach(() => {
// Mock Services
mockLoginStrategyService = mock<LoginStrategyServiceAbstraction>();
@@ -109,6 +115,7 @@ describe("SsoComponent", () => {
mockEnvironmentService = mock<EnvironmentService>();
mockPasswordGenerationService = mock<PasswordGenerationServiceAbstraction>();
mockLogService = mock<LogService>();
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
mockConfigService = mock<ConfigServiceAbstraction>();
// Mock loginStrategyService.logIn params
@@ -124,49 +131,52 @@ describe("SsoComponent", () => {
mockOnSuccessfulLoginForceResetNavigate = jest.fn();
mockOnSuccessfulLoginTdeNavigate = jest.fn();
mockAcctDecryptionOpts = {
noMasterPassword: new AccountDecryptionOptions({
mockUserDecryptionOpts = {
noMasterPassword: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: undefined,
keyConnectorOption: undefined,
}),
withMasterPassword: new AccountDecryptionOptions({
withMasterPassword: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: undefined,
keyConnectorOption: undefined,
}),
withMasterPasswordAndTrustedDevice: new AccountDecryptionOptions({
withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
keyConnectorOption: undefined,
}),
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
keyConnectorOption: undefined,
}),
withMasterPasswordAndKeyConnector: new AccountDecryptionOptions({
withMasterPasswordAndKeyConnector: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: undefined,
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
}),
noMasterPasswordWithTrustedDevice: new AccountDecryptionOptions({
noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
keyConnectorOption: undefined,
}),
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
keyConnectorOption: undefined,
}),
noMasterPasswordWithKeyConnector: new AccountDecryptionOptions({
noMasterPasswordWithKeyConnector: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: undefined,
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
}),
};
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions;
TestBed.configureTestingModule({
declarations: [TestSsoComponent],
providers: [
@@ -183,6 +193,10 @@ describe("SsoComponent", () => {
{ provide: EnvironmentService, useValue: mockEnvironmentService },
{ provide: PasswordGenerationServiceAbstraction, useValue: mockPasswordGenerationService },
{
provide: UserDecryptionOptionsServiceAbstraction,
useValue: mockUserDecryptionOptionsService,
},
{ provide: LogService, useValue: mockLogService },
{ provide: ConfigServiceAbstraction, useValue: mockConfigService },
],
@@ -230,9 +244,7 @@ describe("SsoComponent", () => {
authResult.twoFactorProviders = new Map([[TwoFactorProviderType.Authenticator, {}]]);
// use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPassword,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
mockLoginStrategyService.logIn.mockResolvedValue(authResult);
});
@@ -341,8 +353,8 @@ describe("SsoComponent", () => {
describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => {
let authResult;
beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
);
authResult = new AuthResult();
@@ -377,8 +389,8 @@ describe("SsoComponent", () => {
const reasonString = ForceSetPasswordReason[forceResetPasswordReason];
let authResult;
beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice,
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
);
authResult = new AuthResult();
@@ -394,8 +406,8 @@ describe("SsoComponent", () => {
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => {
let authResult;
beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice,
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
);
authResult = new AuthResult();
@@ -440,9 +452,7 @@ describe("SsoComponent", () => {
describe("Given user needs to set a master password", () => {
beforeEach(() => {
// Only need to test the case where the user has no master password to test the primary change mp flow here
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.noMasterPassword,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword);
});
testChangePasswordOnSuccessfulLogin();
@@ -450,9 +460,7 @@ describe("SsoComponent", () => {
});
it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.noMasterPasswordWithKeyConnector,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPasswordWithKeyConnector);
await _component.logIn(code, codeVerifier, orgIdFromState);
expect(mockLoginStrategyService.logIn).toHaveBeenCalledTimes(1);
@@ -475,9 +483,7 @@ describe("SsoComponent", () => {
beforeEach(() => {
// use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPassword,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
const authResult = new AuthResult();
authResult.forcePasswordReset = forceResetPasswordReason;
@@ -494,9 +500,7 @@ describe("SsoComponent", () => {
const authResult = new AuthResult();
authResult.twoFactorProviders = null;
// use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPassword,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
authResult.forcePasswordReset = ForceSetPasswordReason.None;
mockLoginStrategyService.logIn.mockResolvedValue(authResult);
});

View File

@@ -1,13 +1,19 @@
import { Directive } from "@angular/core";
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators";
import { LoginStrategyServiceAbstraction, SsoLoginCredentials } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
SsoLoginCredentials,
TrustedDeviceUserDecryptionOption,
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@@ -17,7 +23,6 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils";
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
@Directive()
@@ -59,6 +64,7 @@ export class SsoComponent {
protected environmentService: EnvironmentService,
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
protected logService: LogService,
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
protected configService: ConfigServiceAbstraction,
) {}
@@ -194,9 +200,6 @@ export class SsoComponent {
this.formPromise = this.loginStrategyService.logIn(credentials);
const authResult = await this.formPromise;
const acctDecryptionOpts: AccountDecryptionOptions =
await this.stateService.getAccountDecryptionOptions();
if (authResult.requiresTwoFactor) {
return await this.handleTwoFactorRequired(orgSsoIdentifier);
}
@@ -217,15 +220,20 @@ export class SsoComponent {
return await this.handleForcePasswordReset(orgSsoIdentifier);
}
// must come after 2fa check since user decryption options aren't available if 2fa is required
const userDecryptionOpts = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
);
const tdeEnabled = await this.isTrustedDeviceEncEnabled(
acctDecryptionOpts.trustedDeviceOption,
userDecryptionOpts.trustedDeviceOption,
);
if (tdeEnabled) {
return await this.handleTrustedDeviceEncryptionEnabled(
authResult,
orgSsoIdentifier,
acctDecryptionOpts,
userDecryptionOpts,
);
}
@@ -233,8 +241,8 @@ export class SsoComponent {
// have one and they aren't using key connector.
// Note: TDE & Key connector are mutually exclusive org config options.
const requireSetPassword =
!acctDecryptionOpts.hasMasterPassword &&
acctDecryptionOpts.keyConnectorOption === undefined;
!userDecryptionOpts.hasMasterPassword &&
userDecryptionOpts.keyConnectorOption === undefined;
if (requireSetPassword || authResult.resetMasterPassword) {
// Change implies going no password -> password in this case
@@ -270,12 +278,12 @@ export class SsoComponent {
private async handleTrustedDeviceEncryptionEnabled(
authResult: AuthResult,
orgIdentifier: string,
acctDecryptionOpts: AccountDecryptionOptions,
userDecryptionOpts: UserDecryptionOptions,
): Promise<void> {
// If user doesn't have a MP, but has reset password permission, they must set a MP
if (
!acctDecryptionOpts.hasMasterPassword &&
acctDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
!userDecryptionOpts.hasMasterPassword &&
userDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
) {
// Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device)
// Note: we cannot directly navigate in this scenario as we are in a pre-decryption state, and

View File

@@ -3,6 +3,7 @@ import { Router } from "@angular/router";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -19,6 +20,7 @@ export class TwoFactorOptionsComponent implements OnInit {
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected win: Window,
protected environmentService: EnvironmentService,
) {}
ngOnInit() {
@@ -30,7 +32,8 @@ export class TwoFactorOptionsComponent implements OnInit {
}
recover() {
this.platformUtilsService.launchUri("https://vault.bitwarden.com/#/recover-2fa");
const webVault = this.environmentService.getWebVaultUrl();
this.platformUtilsService.launchUri(webVault + "/#/recover-2fa");
this.onRecoverSelected.emit();
}
}

View File

@@ -1,19 +1,24 @@
import { Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute, Router, convertToParamMap } from "@angular/router";
import { MockProxy, mock } from "jest-mock-extended";
import { ActivatedRoute, convertToParamMap, Router } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import {
FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption,
LoginStrategyServiceAbstraction,
FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption,
FakeUserDecryptionOptions as UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { KeyConnectorUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
@@ -22,7 +27,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
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 { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { TwoFactorComponent } from "./two-factor.component";
@@ -56,20 +60,23 @@ describe("TwoFactorComponent", () => {
let mockTwoFactorService: MockProxy<TwoFactorService>;
let mockAppIdService: MockProxy<AppIdService>;
let mockLoginService: MockProxy<LoginService>;
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
let mockAcctDecryptionOpts: {
noMasterPassword: AccountDecryptionOptions;
withMasterPassword: AccountDecryptionOptions;
withMasterPasswordAndTrustedDevice: AccountDecryptionOptions;
withMasterPasswordAndTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
withMasterPasswordAndKeyConnector: AccountDecryptionOptions;
noMasterPasswordWithTrustedDevice: AccountDecryptionOptions;
noMasterPasswordWithTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
noMasterPasswordWithKeyConnector: AccountDecryptionOptions;
let mockUserDecryptionOpts: {
noMasterPassword: UserDecryptionOptions;
withMasterPassword: UserDecryptionOptions;
withMasterPasswordAndTrustedDevice: UserDecryptionOptions;
withMasterPasswordAndTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
withMasterPasswordAndKeyConnector: UserDecryptionOptions;
noMasterPasswordWithTrustedDevice: UserDecryptionOptions;
noMasterPasswordWithTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
noMasterPasswordWithKeyConnector: UserDecryptionOptions;
};
let selectedUserDecryptionOptions: BehaviorSubject<UserDecryptionOptions>;
beforeEach(() => {
mockLoginStrategyService = mock<LoginStrategyServiceAbstraction>();
mockRouter = mock<Router>();
@@ -83,52 +90,56 @@ describe("TwoFactorComponent", () => {
mockTwoFactorService = mock<TwoFactorService>();
mockAppIdService = mock<AppIdService>();
mockLoginService = mock<LoginService>();
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
mockConfigService = mock<ConfigServiceAbstraction>();
mockAcctDecryptionOpts = {
noMasterPassword: new AccountDecryptionOptions({
mockUserDecryptionOpts = {
noMasterPassword: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: undefined,
keyConnectorOption: undefined,
}),
withMasterPassword: new AccountDecryptionOptions({
withMasterPassword: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: undefined,
keyConnectorOption: undefined,
}),
withMasterPasswordAndTrustedDevice: new AccountDecryptionOptions({
withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
keyConnectorOption: undefined,
}),
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
keyConnectorOption: undefined,
}),
withMasterPasswordAndKeyConnector: new AccountDecryptionOptions({
withMasterPasswordAndKeyConnector: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: undefined,
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
}),
noMasterPasswordWithTrustedDevice: new AccountDecryptionOptions({
noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
keyConnectorOption: undefined,
}),
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
keyConnectorOption: undefined,
}),
noMasterPasswordWithKeyConnector: new AccountDecryptionOptions({
noMasterPasswordWithKeyConnector: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: undefined,
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
}),
};
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions;
TestBed.configureTestingModule({
declarations: [TestTwoFactorComponent],
providers: [
@@ -153,6 +164,10 @@ describe("TwoFactorComponent", () => {
{ provide: TwoFactorService, useValue: mockTwoFactorService },
{ provide: AppIdService, useValue: mockAppIdService },
{ provide: LoginService, useValue: mockLoginService },
{
provide: UserDecryptionOptionsServiceAbstraction,
useValue: mockUserDecryptionOptionsService,
},
{ provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService },
{ provide: ConfigServiceAbstraction, useValue: mockConfigService },
],
@@ -213,9 +228,7 @@ describe("TwoFactorComponent", () => {
component.remember = remember;
component.captchaToken = captchaToken;
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPassword,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
});
it("calls authService.logInTwoFactor with correct parameters when form is submitted", async () => {
@@ -289,17 +302,15 @@ describe("TwoFactorComponent", () => {
describe("Given user needs to set a master password", () => {
beforeEach(() => {
// Only need to test the case where the user has no master password to test the primary change mp flow here
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.noMasterPassword,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword);
});
testChangePasswordOnSuccessfulLogin();
});
it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.noMasterPasswordWithKeyConnector,
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
);
await component.doSubmit();
@@ -321,9 +332,7 @@ describe("TwoFactorComponent", () => {
beforeEach(() => {
// use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPassword,
);
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
const authResult = new AuthResult();
authResult.forcePasswordReset = forceResetPasswordReason;
@@ -385,8 +394,8 @@ describe("TwoFactorComponent", () => {
describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => {
beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
);
const authResult = new AuthResult();
@@ -420,8 +429,8 @@ describe("TwoFactorComponent", () => {
beforeEach(() => {
// use standard user with MP because this test is not concerned with password reset.
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice,
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
);
const authResult = new AuthResult();
@@ -436,8 +445,8 @@ describe("TwoFactorComponent", () => {
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => {
let authResult;
beforeEach(() => {
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice,
selectedUserDecryptionOptions.next(
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
);
authResult = new AuthResult();

View File

@@ -6,7 +6,12 @@ import { first } from "rxjs/operators";
// eslint-disable-next-line no-restricted-imports
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
TrustedDeviceUserDecryptionOption,
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
@@ -15,7 +20,6 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
@@ -27,7 +31,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
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 { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
import { CaptchaProtectedComponent } from "./captcha-protected.component";
@@ -86,6 +89,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
protected twoFactorService: TwoFactorService,
protected appIdService: AppIdService,
protected loginService: LoginService,
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected configService: ConfigServiceAbstraction,
) {
@@ -290,22 +294,23 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
return await this.handleForcePasswordReset(this.orgIdentifier);
}
const acctDecryptionOpts: AccountDecryptionOptions =
await this.stateService.getAccountDecryptionOptions();
const userDecryptionOpts = await firstValueFrom(
this.userDecryptionOptionsService.userDecryptionOptions$,
);
const tdeEnabled = await this.isTrustedDeviceEncEnabled(acctDecryptionOpts.trustedDeviceOption);
const tdeEnabled = await this.isTrustedDeviceEncEnabled(userDecryptionOpts.trustedDeviceOption);
if (tdeEnabled) {
return await this.handleTrustedDeviceEncryptionEnabled(
authResult,
this.orgIdentifier,
acctDecryptionOpts,
userDecryptionOpts,
);
}
// User must set password if they don't have one and they aren't using either TDE or key connector.
const requireSetPassword =
!acctDecryptionOpts.hasMasterPassword && acctDecryptionOpts.keyConnectorOption === undefined;
!userDecryptionOpts.hasMasterPassword && userDecryptionOpts.keyConnectorOption === undefined;
if (requireSetPassword || authResult.resetMasterPassword) {
// Change implies going no password -> password in this case
@@ -326,12 +331,12 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
private async handleTrustedDeviceEncryptionEnabled(
authResult: AuthResult,
orgIdentifier: string,
acctDecryptionOpts: AccountDecryptionOptions,
userDecryptionOpts: UserDecryptionOptions,
): Promise<void> {
// If user doesn't have a MP, but has reset password permission, they must set a MP
if (
!acctDecryptionOpts.hasMasterPassword &&
acctDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
!userDecryptionOpts.hasMasterPassword &&
userDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
) {
// Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device)
// Note: we cannot directly navigate to the set password screen in this scenario as we are in a pre-decryption state, and

View File

@@ -53,7 +53,7 @@ export function lockGuard(): CanActivateFn {
// User is authN and in locked state.
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$);
// Create special exception which allows users to go from the login-initiated page to the lock page for the approve w/ MP flow
// The MP check is necessary to prevent direct manual navigation from other locked state pages for users who don't have a MP

View File

@@ -46,7 +46,7 @@ export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActiv
// If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the
// login decryption options component.
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$);
const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$);
if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) {
return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams });

View File

@@ -26,7 +26,7 @@ export function tdeDecryptionRequiredGuard(): CanActivateFn {
const router = inject(Router);
const authStatus = await authService.getAuthStatus();
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$);
const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$);
if (authStatus !== AuthenticationStatus.Locked || !tdeEnabled || everHadUserKey) {
return router.createUrlTree(["/"]);

View File

@@ -29,7 +29,6 @@ import { UserTypePipe } from "./pipes/user-type.pipe";
import { EllipsisPipe } from "./platform/pipes/ellipsis.pipe";
import { FingerprintPipe } from "./platform/pipes/fingerprint.pipe";
import { I18nPipe } from "./platform/pipes/i18n.pipe";
import { ExportScopeCalloutComponent } from "./tools/export/components/export-scope-callout.component";
import { PasswordStrengthComponent } from "./tools/password-strength/password-strength.component";
import { IconComponent } from "./vault/components/icon.component";
@@ -54,7 +53,6 @@ import { IconComponent } from "./vault/components/icon.component";
CopyTextDirective,
CreditCardNumberPipe,
EllipsisPipe,
ExportScopeCalloutComponent,
FallbackSrcDirective,
I18nPipe,
IconComponent,
@@ -85,7 +83,6 @@ import { IconComponent } from "./vault/components/icon.component";
CopyTextDirective,
CreditCardNumberPipe,
EllipsisPipe,
ExportScopeCalloutComponent,
FallbackSrcDirective,
I18nPipe,
IconComponent,

View File

@@ -8,6 +8,9 @@ import {
PinCryptoService,
LoginStrategyServiceAbstraction,
LoginStrategyService,
InternalUserDecryptionOptionsServiceAbstraction,
UserDecryptionOptionsService,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
@@ -27,6 +30,7 @@ import {
OrgDomainInternalServiceAbstraction,
OrgDomainServiceAbstraction,
} from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain.service.abstraction";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import {
@@ -38,6 +42,7 @@ import { OrganizationApiService } from "@bitwarden/common/admin-console/services
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
import { OrgDomainApiService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain-api.service";
import { OrgDomainService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain.service";
import { DefaultOrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/services/organization-management-preferences/default-organization-management-preferences.service";
import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation";
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
@@ -241,8 +246,8 @@ import { safeProvider, SafeProvider } from "../platform/utils/safe-provider";
import {
LOCALES_DIRECTORY,
LOCKED_CALLBACK,
LOG_MAC_FAILURES,
LOGOUT_CALLBACK,
LOG_MAC_FAILURES,
MEMORY_STORAGE,
OBSERVABLE_DISK_STORAGE,
OBSERVABLE_MEMORY_STORAGE,
@@ -367,6 +372,7 @@ const typesafeProviders: Array<SafeProvider> = [
PolicyServiceAbstraction,
DeviceTrustCryptoServiceAbstraction,
AuthRequestServiceAbstraction,
InternalUserDecryptionOptionsServiceAbstraction,
GlobalStateProvider,
BillingAccountProfileStateService,
],
@@ -475,6 +481,15 @@ const typesafeProviders: Array<SafeProvider> = [
useClass: EnvironmentService,
deps: [StateProvider, AccountServiceAbstraction],
}),
safeProvider({
provide: InternalUserDecryptionOptionsServiceAbstraction,
useClass: UserDecryptionOptionsService,
deps: [StateProvider],
}),
safeProvider({
provide: UserDecryptionOptionsServiceAbstraction,
useExisting: InternalUserDecryptionOptionsServiceAbstraction,
}),
safeProvider({
provide: TotpServiceAbstraction,
useClass: TotpService,
@@ -575,6 +590,7 @@ const typesafeProviders: Array<SafeProvider> = [
FolderApiServiceAbstraction,
InternalOrganizationServiceAbstraction,
SendApiServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
AvatarServiceAbstraction,
LOGOUT_CALLBACK,
BillingAccountProfileStateService,
@@ -585,6 +601,7 @@ const typesafeProviders: Array<SafeProvider> = [
provide: VaultTimeoutSettingsServiceAbstraction,
useClass: VaultTimeoutSettingsService,
deps: [
UserDecryptionOptionsServiceAbstraction,
CryptoServiceAbstraction,
TokenServiceAbstraction,
PolicyServiceAbstraction,
@@ -763,6 +780,7 @@ const typesafeProviders: Array<SafeProvider> = [
CryptoServiceAbstraction,
I18nServiceAbstraction,
UserVerificationApiServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
PinCryptoServiceAbstraction,
LogService,
VaultTimeoutSettingsServiceAbstraction,
@@ -900,6 +918,7 @@ const typesafeProviders: Array<SafeProvider> = [
DevicesApiServiceAbstraction,
I18nServiceAbstraction,
PlatformUtilsServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
],
}),
safeProvider({
@@ -1048,6 +1067,11 @@ const typesafeProviders: Array<SafeProvider> = [
useClass: DefaultBillingAccountProfileStateService,
deps: [ActiveUserStateProvider],
}),
safeProvider({
provide: OrganizationManagementPreferencesService,
useClass: DefaultOrganizationManagementPreferencesService,
deps: [StateProvider],
}),
];
function encryptServiceFactory(

View File

@@ -18,12 +18,12 @@
{{ dialogOptions.bodyText | i18n }}
</p>
<app-callout
<bit-callout
*ngIf="dialogOptions.calloutOptions"
[type]="dialogOptions.calloutOptions.type"
>
{{ dialogOptions.calloutOptions.text | i18n }}
</app-callout>
</bit-callout>
</ng-container>
<!-- Shown when client side verification methods picked and no verification methods found -->

View File

@@ -12,6 +12,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import {
AsyncActionsModule,
ButtonModule,
CalloutModule,
DialogModule,
DialogService,
} from "@bitwarden/components";
@@ -34,6 +35,7 @@ import { UserVerificationFormInputComponent } from "./user-verification-form-inp
DialogModule,
AsyncActionsModule,
UserVerificationFormInputComponent,
CalloutModule,
],
})
export class UserVerificationDialogComponent {

View File

@@ -11,9 +11,9 @@ export type UserVerificationCalloutOptions = {
/**
* The type of the callout.
* Can be "warning", "danger", "error", or "tip".
* Can be "warning", "danger", "info", or "success".
*/
type: "warning" | "danger" | "error" | "tip";
type: "warning" | "danger" | "info" | "success";
};
/**

View File

@@ -49,12 +49,12 @@
</div>
</div>
<app-callout type="error" *ngIf="biometricsVerificationFailed">
<bit-callout type="danger" *ngIf="biometricsVerificationFailed">
{{ "couldNotCompleteBiometrics" | i18n }}
<button bitLink type="button" linkType="primary" (click)="verifyUserViaBiometrics()">
{{ "tryAgain" | i18n }}
</button>
</app-callout>
</bit-callout>
</ng-container>
<!-- Alternate verification options if user has more than 1 -->

View File

@@ -20,6 +20,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
AsyncActionsModule,
ButtonModule,
CalloutModule,
FormFieldModule,
IconButtonModule,
IconModule,
@@ -62,6 +63,7 @@ import { ActiveClientVerificationOption } from "./active-client-verification-opt
IconModule,
LinkModule,
ButtonModule,
CalloutModule,
],
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil

View File

@@ -1,3 +1,4 @@
export * from "./pin-crypto.service.abstraction";
export * from "./login-strategy.service";
export * from "./user-decryption-options.service.abstraction";
export * from "./auth-request.service.abstraction";

View File

@@ -0,0 +1,34 @@
import { Observable } from "rxjs";
import { UserDecryptionOptions } from "../models";
export abstract class UserDecryptionOptionsServiceAbstraction {
/**
* Returns what decryption options are available for the current user.
* @remark This is sent from the server on authentication.
*/
abstract userDecryptionOptions$: Observable<UserDecryptionOptions>;
/**
* Uses user decryption options to determine if current user has a master password.
* @remark This is sent from the server, and does not indicate if the master password
* was used to login and/or if a master key is saved locally.
*/
abstract hasMasterPassword$: Observable<boolean>;
/**
* Returns the user decryption options for the given user id.
* @param userId The user id to check.
*/
abstract userDecryptionOptionsById$(userId: string): Observable<UserDecryptionOptions>;
}
export abstract class InternalUserDecryptionOptionsServiceAbstraction extends UserDecryptionOptionsServiceAbstraction {
/**
* Sets the current decryption options for the user, contains the current configuration
* of the users account related to how they can decrypt their vault.
* @remark Intended to be used when user decryption options are received from server, does
* not update the server. Consider syncing instead of updating locally.
* @param userDecryptionOptions Current user decryption options received from server.
*/
abstract setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise<void>;
}

View File

@@ -17,6 +17,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
import {
@@ -37,6 +38,7 @@ describe("AuthRequestLoginStrategy", () => {
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptions: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
@@ -65,6 +67,7 @@ describe("AuthRequestLoginStrategy", () => {
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
userDecryptionOptions = mock<InternalUserDecryptionOptionsServiceAbstraction>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
@@ -83,6 +86,7 @@ describe("AuthRequestLoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptions,
deviceTrustCryptoService,
billingAccountProfileStateService,
);

View File

@@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
@@ -54,6 +55,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
@@ -67,6 +69,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);

View File

@@ -28,7 +28,6 @@ import {
AccountProfile,
AccountTokens,
AccountKeys,
AccountDecryptionOptions,
} from "@bitwarden/common/platform/models/domain/account";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
@@ -39,8 +38,10 @@ import {
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserKey, MasterKey, DeviceKey } from "@bitwarden/common/types/key";
import { LoginStrategyServiceAbstraction } from "../abstractions/login-strategy.service";
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
import { LoginStrategyServiceAbstraction } from "../abstractions";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { PasswordLoginCredentials } from "../models";
import { UserDecryptionOptions } from "../models/domain/user-decryption-options";
import { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy";
@@ -108,6 +109,7 @@ describe("LoginStrategy", () => {
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let policyService: MockProxy<PolicyService>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
@@ -126,7 +128,7 @@ describe("LoginStrategy", () => {
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
policyService = mock<PolicyService>();
passwordStrengthService = mock<PasswordStrengthService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
@@ -146,6 +148,7 @@ describe("LoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
passwordStrengthService,
policyService,
loginStrategyService,
@@ -204,9 +207,11 @@ describe("LoginStrategy", () => {
...new AccountTokens(),
},
keys: new AccountKeys(),
decryptionOptions: AccountDecryptionOptions.fromResponse(idTokenResponse),
}),
);
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
UserDecryptionOptions.fromResponse(idTokenResponse),
);
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
});
@@ -409,6 +414,7 @@ describe("LoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
passwordStrengthService,
policyService,
loginStrategyService,

View File

@@ -30,9 +30,9 @@ import {
Account,
AccountProfile,
AccountTokens,
AccountDecryptionOptions,
} from "@bitwarden/common/platform/models/domain/account";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import {
UserApiLoginCredentials,
PasswordLoginCredentials,
@@ -40,6 +40,7 @@ import {
AuthRequestLoginCredentials,
WebAuthnLoginCredentials,
} from "../models/domain/login-credentials";
import { UserDecryptionOptions } from "../models/domain/user-decryption-options";
import { CacheData } from "../services/login-strategies/login-strategy.state";
type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse;
@@ -69,6 +70,7 @@ export abstract class LoginStrategy {
protected logService: LogService,
protected stateService: StateService,
protected twoFactorService: TwoFactorService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
protected billingAccountProfileStateService: BillingAccountProfileStateService,
) {}
@@ -203,11 +205,14 @@ export abstract class LoginStrategy {
...new AccountTokens(),
},
keys: accountKeys,
decryptionOptions: AccountDecryptionOptions.fromResponse(tokenResponse),
adminAuthRequest: adminAuthRequest?.toJSON(),
}),
);
await this.userDecryptionOptionsService.setUserDecryptionOptions(
UserDecryptionOptions.fromResponse(tokenResponse),
);
await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false);
}

View File

@@ -27,6 +27,7 @@ import { CsprngArray } from "@bitwarden/common/types/csprng";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { LoginStrategyServiceAbstraction } from "../abstractions";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec";
@@ -60,6 +61,7 @@ describe("PasswordLoginStrategy", () => {
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let policyService: MockProxy<PolicyService>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
@@ -79,6 +81,7 @@ describe("PasswordLoginStrategy", () => {
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
policyService = mock<PolicyService>();
passwordStrengthService = mock<PasswordStrengthService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
@@ -108,6 +111,7 @@ describe("PasswordLoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
passwordStrengthService,
policyService,
loginStrategyService,

View File

@@ -26,6 +26,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass
import { MasterKey } from "@bitwarden/common/types/key";
import { LoginStrategyServiceAbstraction } from "../abstractions";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
@@ -84,6 +85,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
logService: LogService,
protected stateService: StateService,
twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private passwordStrengthService: PasswordStrengthServiceAbstraction,
private policyService: PolicyService,
private loginStrategyService: LoginStrategyServiceAbstraction,
@@ -99,6 +101,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);

View File

@@ -23,7 +23,10 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { DeviceKey, UserKey, MasterKey } from "@bitwarden/common/types/key";
import { AuthRequestServiceAbstraction } from "../abstractions";
import {
AuthRequestServiceAbstraction,
InternalUserDecryptionOptionsServiceAbstraction,
} from "../abstractions";
import { SsoLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec";
@@ -39,6 +42,7 @@ describe("SsoLoginStrategy", () => {
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let keyConnectorService: MockProxy<KeyConnectorService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
@@ -66,6 +70,7 @@ describe("SsoLoginStrategy", () => {
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
keyConnectorService = mock<KeyConnectorService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
authRequestService = mock<AuthRequestServiceAbstraction>();
@@ -87,6 +92,7 @@ describe("SsoLoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
keyConnectorService,
deviceTrustCryptoService,
authRequestService,

View File

@@ -21,7 +21,10 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { AuthRequestServiceAbstraction } from "../abstractions";
import {
InternalUserDecryptionOptionsServiceAbstraction,
AuthRequestServiceAbstraction,
} from "../abstractions";
import { SsoLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
@@ -84,6 +87,7 @@ export class SsoLoginStrategy extends LoginStrategy {
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private keyConnectorService: KeyConnectorService,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction,
@@ -100,6 +104,7 @@ export class SsoLoginStrategy extends LoginStrategy {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);

View File

@@ -18,6 +18,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec";
@@ -35,6 +36,7 @@ describe("UserApiLoginStrategy", () => {
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let keyConnectorService: MockProxy<KeyConnectorService>;
let environmentService: MockProxy<EnvironmentService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
@@ -57,6 +59,7 @@ describe("UserApiLoginStrategy", () => {
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
keyConnectorService = mock<KeyConnectorService>();
environmentService = mock<EnvironmentService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
@@ -76,6 +79,7 @@ describe("UserApiLoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
environmentService,
keyConnectorService,
billingAccountProfileStateService,

View File

@@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
@@ -47,6 +48,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private environmentService: EnvironmentService,
private keyConnectorService: KeyConnectorService,
billingAccountProfileStateService: BillingAccountProfileStateService,
@@ -61,6 +63,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);
this.cache = new BehaviorSubject(data);

View File

@@ -18,6 +18,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PrfKey, UserKey } from "@bitwarden/common/types/key";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec";
@@ -35,6 +36,7 @@ describe("WebAuthnLoginStrategy", () => {
let logService!: MockProxy<LogService>;
let stateService!: MockProxy<StateService>;
let twoFactorService!: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let webAuthnLoginStrategy!: WebAuthnLoginStrategy;
@@ -70,6 +72,7 @@ describe("WebAuthnLoginStrategy", () => {
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
tokenService.getTwoFactorToken.mockResolvedValue(null);
@@ -87,6 +90,7 @@ describe("WebAuthnLoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);

View File

@@ -17,6 +17,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserKey } from "@bitwarden/common/types/key";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions";
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
@@ -49,6 +50,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
super(
@@ -61,6 +63,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);

View File

@@ -1,2 +1,3 @@
export * from "./rotateable-key-set";
export * from "./login-credentials";
export * from "./user-decryption-options";

View File

@@ -0,0 +1,153 @@
import { Jsonify } from "type-fest";
import { KeyConnectorUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/key-connector-user-decryption-option.response";
import { TrustedDeviceUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/trusted-device-user-decryption-option.response";
import { IdentityTokenResponse } from "@bitwarden/common/src/auth/models/response/identity-token.response";
/**
* Key Connector decryption options. Intended to be sent to the client for use after authentication.
* @see {@link UserDecryptionOptions}
*/
export class KeyConnectorUserDecryptionOption {
/** The URL of the key connector configured for this user. */
keyConnectorUrl: string;
/**
* Initializes a new instance of the KeyConnectorUserDecryptionOption from a response object.
* @param response The key connector user decryption option response object.
* @returns A new instance of the KeyConnectorUserDecryptionOption. Will initialize even if the response is nullish.
*/
static fromResponse(
response: KeyConnectorUserDecryptionOptionResponse,
): KeyConnectorUserDecryptionOption {
const options = new KeyConnectorUserDecryptionOption();
options.keyConnectorUrl = response?.keyConnectorUrl ?? null;
return options;
}
/**
* Initializes a new instance of a KeyConnectorUserDecryptionOption from a JSON object.
* @param obj JSON object to deserialize.
* @returns A new instance of the KeyConnectorUserDecryptionOption. Will initialize even if the JSON object is nullish.
*/
static fromJSON(
obj: Jsonify<KeyConnectorUserDecryptionOption>,
): KeyConnectorUserDecryptionOption {
return Object.assign(new KeyConnectorUserDecryptionOption(), obj);
}
}
/**
* Trusted device decryption options. Intended to be sent to the client for use after authentication.
* @see {@link UserDecryptionOptions}
*/
export class TrustedDeviceUserDecryptionOption {
/** True if an admin has approved an admin auth request previously made from this device. */
hasAdminApproval: boolean;
/** True if the user has a device capable of approving an auth request. */
hasLoginApprovingDevice: boolean;
/** True if the user has manage reset password permission, as these users must be forced to have a master password. */
hasManageResetPasswordPermission: boolean;
/**
* Initializes a new instance of the TrustedDeviceUserDecryptionOption from a response object.
* @param response The trusted device user decryption option response object.
* @returns A new instance of the TrustedDeviceUserDecryptionOption. Will initialize even if the response is nullish.
*/
static fromResponse(
response: TrustedDeviceUserDecryptionOptionResponse,
): TrustedDeviceUserDecryptionOption {
const options = new TrustedDeviceUserDecryptionOption();
options.hasAdminApproval = response?.hasAdminApproval ?? false;
options.hasLoginApprovingDevice = response?.hasLoginApprovingDevice ?? false;
options.hasManageResetPasswordPermission = response?.hasManageResetPasswordPermission ?? false;
return options;
}
/**
* Initializes a new instance of the TrustedDeviceUserDecryptionOption from a JSON object.
* @param obj JSON object to deserialize.
* @returns A new instance of the TrustedDeviceUserDecryptionOption. Will initialize even if the JSON object is nullish.
*/
static fromJSON(
obj: Jsonify<TrustedDeviceUserDecryptionOption>,
): TrustedDeviceUserDecryptionOption {
return Object.assign(new TrustedDeviceUserDecryptionOption(), obj);
}
}
/**
* Represents the decryption options the user has configured on the server. This is intended to be sent
* to the client on authentication, and can be used to determine how to decrypt the user's vault.
*/
export class UserDecryptionOptions {
/** True if the user has a master password configured on the server. */
hasMasterPassword: boolean;
/** {@link TrustedDeviceUserDecryptionOption} */
trustedDeviceOption?: TrustedDeviceUserDecryptionOption;
/** {@link KeyConnectorUserDecryptionOption} */
keyConnectorOption?: KeyConnectorUserDecryptionOption;
/**
* Initializes a new instance of the UserDecryptionOptions from a response object.
* @param response user decryption options response object
* @returns A new instance of the UserDecryptionOptions.
* @throws If the response is nullish, this method will throw an error. User decryption options
* are required for client initialization.
*/
// TODO: Change response type to `UserDecryptionOptionsResponse` after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
static fromResponse(response: IdentityTokenResponse): UserDecryptionOptions {
if (response == null) {
throw new Error("User Decryption Options are required for client initialization.");
}
const decryptionOptions = new UserDecryptionOptions();
if (response.userDecryptionOptions) {
// If the response has userDecryptionOptions, this means it's on a post-TDE server version and can interrogate
// the new decryption options.
const responseOptions = response.userDecryptionOptions;
decryptionOptions.hasMasterPassword = responseOptions.hasMasterPassword;
decryptionOptions.trustedDeviceOption = TrustedDeviceUserDecryptionOption.fromResponse(
responseOptions.trustedDeviceOption,
);
decryptionOptions.keyConnectorOption = KeyConnectorUserDecryptionOption.fromResponse(
responseOptions.keyConnectorOption,
);
} else {
// If the response does not have userDecryptionOptions, this means it's on a pre-TDE server version and so
// we must base our decryption options on the presence of the keyConnectorUrl.
// Note that the presence of keyConnectorUrl implies that the user does not have a master password, as in pre-TDE
// server versions, a master password short-circuited the addition of the keyConnectorUrl to the response.
// TODO: remove this check after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
const usingKeyConnector = response.keyConnectorUrl != null;
decryptionOptions.hasMasterPassword = !usingKeyConnector;
if (usingKeyConnector) {
decryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption();
decryptionOptions.keyConnectorOption.keyConnectorUrl = response.keyConnectorUrl;
}
}
return decryptionOptions;
}
/**
* Initializes a new instance of the UserDecryptionOptions from a JSON object.
* @param obj JSON object to deserialize.
* @returns A new instance of the UserDecryptionOptions. Will initialize even if the JSON object is nullish.
*/
static fromJSON(obj: Jsonify<UserDecryptionOptions>): UserDecryptionOptions {
const decryptionOptions = Object.assign(new UserDecryptionOptions(), obj);
decryptionOptions.trustedDeviceOption = TrustedDeviceUserDecryptionOption.fromJSON(
obj?.trustedDeviceOption,
);
decryptionOptions.keyConnectorOption = KeyConnectorUserDecryptionOption.fromJSON(
obj?.keyConnectorOption,
);
return decryptionOptions;
}
}

View File

@@ -1 +1,2 @@
export * from "./domain";
export * from "./spec";

View File

@@ -0,0 +1,38 @@
import {
KeyConnectorUserDecryptionOption,
TrustedDeviceUserDecryptionOption,
UserDecryptionOptions,
} from "../domain";
// To discourage creating new user decryption options, we don't expose a constructor.
// These helpers are for testing purposes only.
/** Testing helper for creating new instances of `UserDecryptionOptions` */
export class FakeUserDecryptionOptions extends UserDecryptionOptions {
constructor(init: Partial<UserDecryptionOptions>) {
super();
Object.assign(this, init);
}
}
/** Testing helper for creating new instances of `KeyConnectorUserDecryptionOption` */
export class FakeKeyConnectorUserDecryptionOption extends KeyConnectorUserDecryptionOption {
constructor(keyConnectorUrl: string) {
super();
this.keyConnectorUrl = keyConnectorUrl;
}
}
/** Testing helper for creating new instances of `TrustedDeviceUserDecryptionOption` */
export class FakeTrustedDeviceUserDecryptionOption extends TrustedDeviceUserDecryptionOption {
constructor(
hasAdminApproval: boolean,
hasLoginApprovingDevice: boolean,
hasManageResetPasswordPermission: boolean,
) {
super();
this.hasAdminApproval = hasAdminApproval;
this.hasLoginApprovingDevice = hasLoginApprovingDevice;
this.hasManageResetPasswordPermission = hasManageResetPasswordPermission;
}
}

View File

@@ -0,0 +1 @@
export * from "./fake-user-decryption-options";

View File

@@ -1,3 +1,4 @@
export * from "./pin-crypto/pin-crypto.service.implementation";
export * from "./login-strategies/login-strategy.service";
export * from "./user-decryption-options/user-decryption-options.service";
export * from "./auth-request/auth-request.service";

View File

@@ -25,8 +25,12 @@ import { KdfType } from "@bitwarden/common/platform/enums";
import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { AuthRequestServiceAbstraction } from "../../abstractions";
import {
AuthRequestServiceAbstraction,
InternalUserDecryptionOptionsServiceAbstraction,
} from "../../abstractions";
import { PasswordLoginCredentials } from "../../models";
import { UserDecryptionOptionsService } from "../user-decryption-options/user-decryption-options.service";
import { LoginStrategyService } from "./login-strategy.service";
import { CACHE_EXPIRATION_KEY } from "./login-strategy.state";
@@ -51,6 +55,7 @@ describe("LoginStrategyService", () => {
let policyService: MockProxy<PolicyService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let stateProvider: FakeGlobalStateProvider;
@@ -74,6 +79,7 @@ describe("LoginStrategyService", () => {
policyService = mock<PolicyService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
authRequestService = mock<AuthRequestServiceAbstraction>();
userDecryptionOptionsService = mock<UserDecryptionOptionsService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
stateProvider = new FakeGlobalStateProvider();
@@ -95,6 +101,7 @@ describe("LoginStrategyService", () => {
policyService,
deviceTrustCryptoService,
authRequestService,
userDecryptionOptionsService,
stateProvider,
billingAccountProfileStateService,
);

View File

@@ -40,6 +40,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass
import { MasterKey } from "@bitwarden/common/types/key";
import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
import { AuthRequestLoginStrategy } from "../../login-strategies/auth-request-login.strategy";
import { PasswordLoginStrategy } from "../../login-strategies/password-login.strategy";
import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy";
@@ -101,6 +102,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
protected policyService: PolicyService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected authRequestService: AuthRequestServiceAbstraction,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
protected stateProvider: GlobalStateProvider,
protected billingAccountProfileStateService: BillingAccountProfileStateService,
) {
@@ -354,6 +356,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.userDecryptionOptionsService,
this.passwordStrengthService,
this.policyService,
this,
@@ -371,6 +374,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.userDecryptionOptionsService,
this.keyConnectorService,
this.deviceTrustCryptoService,
this.authRequestService,
@@ -389,6 +393,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.userDecryptionOptionsService,
this.environmentService,
this.keyConnectorService,
this.billingAccountProfileStateService,
@@ -405,6 +410,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.userDecryptionOptionsService,
this.deviceTrustCryptoService,
this.billingAccountProfileStateService,
);
@@ -420,6 +426,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.userDecryptionOptionsService,
this.billingAccountProfileStateService,
);
}

View File

@@ -0,0 +1,94 @@
import { firstValueFrom } from "rxjs";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
FakeAccountService,
FakeStateProvider,
mockAccountServiceWith,
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import {
USER_DECRYPTION_OPTIONS,
UserDecryptionOptionsService,
} from "./user-decryption-options.service";
describe("UserDecryptionOptionsService", () => {
let sut: UserDecryptionOptionsService;
const fakeUserId = Utils.newGuid() as UserId;
let fakeAccountService: FakeAccountService;
let fakeStateProvider: FakeStateProvider;
beforeEach(() => {
fakeAccountService = mockAccountServiceWith(fakeUserId);
fakeStateProvider = new FakeStateProvider(fakeAccountService);
sut = new UserDecryptionOptionsService(fakeStateProvider);
});
const userDecryptionOptions = {
hasMasterPassword: true,
trustedDeviceOption: {
hasAdminApproval: false,
hasLoginApprovingDevice: false,
hasManageResetPasswordPermission: true,
},
keyConnectorOption: {
keyConnectorUrl: "https://keyconnector.bitwarden.com",
},
};
describe("userDecryptionOptions$", () => {
it("should return the active user's decryption options", async () => {
await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions);
const result = await firstValueFrom(sut.userDecryptionOptions$);
expect(result).toEqual(userDecryptionOptions);
});
});
describe("hasMasterPassword$", () => {
it("should return the hasMasterPassword property of the active user's decryption options", async () => {
await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions);
const result = await firstValueFrom(sut.hasMasterPassword$);
expect(result).toBe(true);
});
});
describe("userDecryptionOptionsById$", () => {
it("should return the user decryption options for the given user", async () => {
const givenUser = Utils.newGuid() as UserId;
await fakeAccountService.addAccount(givenUser, {
name: "Test User 1",
email: "test1@email.com",
status: AuthenticationStatus.Locked,
});
await fakeStateProvider.setUserState(
USER_DECRYPTION_OPTIONS,
userDecryptionOptions,
givenUser,
);
const result = await firstValueFrom(sut.userDecryptionOptionsById$(givenUser));
expect(result).toEqual(userDecryptionOptions);
});
});
describe("setUserDecryptionOptions", () => {
it("should set the active user's decryption options", async () => {
await sut.setUserDecryptionOptions(userDecryptionOptions);
const result = await firstValueFrom(
fakeStateProvider.getActive(USER_DECRYPTION_OPTIONS).state$,
);
expect(result).toEqual(userDecryptionOptions);
});
});
});

View File

@@ -0,0 +1,47 @@
import { map } from "rxjs";
import {
ActiveUserState,
StateProvider,
USER_DECRYPTION_OPTIONS_DISK,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/src/types/guid";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
import { UserDecryptionOptions } from "../../models";
export const USER_DECRYPTION_OPTIONS = new UserKeyDefinition<UserDecryptionOptions>(
USER_DECRYPTION_OPTIONS_DISK,
"decryptionOptions",
{
deserializer: (decryptionOptions) => UserDecryptionOptions.fromJSON(decryptionOptions),
clearOn: ["logout"],
},
);
export class UserDecryptionOptionsService
implements InternalUserDecryptionOptionsServiceAbstraction
{
private userDecryptionOptionsState: ActiveUserState<UserDecryptionOptions>;
userDecryptionOptions$;
hasMasterPassword$;
constructor(private stateProvider: StateProvider) {
this.userDecryptionOptionsState = this.stateProvider.getActive(USER_DECRYPTION_OPTIONS);
this.userDecryptionOptions$ = this.userDecryptionOptionsState.state$;
this.hasMasterPassword$ = this.userDecryptionOptions$.pipe(
map((options) => options?.hasMasterPassword ?? false),
);
}
userDecryptionOptionsById$(userId: UserId) {
return this.stateProvider.getUser(userId, USER_DECRYPTION_OPTIONS).state$;
}
async setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise<void> {
await this.userDecryptionOptionsState.update((_) => userDecryptionOptions);
}
}

View File

@@ -0,0 +1,22 @@
import { Observable } from "rxjs";
/**
* Manages the state of a single organization management preference.
* Can be used to subscribe to or update a given property.
*/
export class OrganizationManagementPreference<T> {
state$: Observable<T>;
set: (value: T) => Promise<void>;
constructor(state$: Observable<T>, setFn: (value: T) => Promise<void>) {
this.state$ = state$;
this.set = setFn;
}
}
/**
* Publishes state of a given user's personal settings relating to the user experience of managing an organization.
*/
export abstract class OrganizationManagementPreferencesService {
autoConfirmFingerPrints: OrganizationManagementPreference<boolean>;
}

View File

@@ -0,0 +1,44 @@
import { MockProxy } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
import { UserId } from "../../../types/guid";
import { DefaultOrganizationManagementPreferencesService } from "./default-organization-management-preferences.service";
describe("OrganizationManagementPreferencesService", () => {
let stateProvider: FakeStateProvider;
let organizationManagementPreferencesService: MockProxy<DefaultOrganizationManagementPreferencesService>;
beforeEach(() => {
const accountService = mockAccountServiceWith("userId" as UserId);
stateProvider = new FakeStateProvider(accountService);
organizationManagementPreferencesService = new DefaultOrganizationManagementPreferencesService(
stateProvider,
);
});
describe("autoConfirmFingerPrints", () => {
it("returns false by default", async () => {
const value = await firstValueFrom(
organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
);
expect(value).toEqual(false);
});
it("returns true if set", async () => {
await organizationManagementPreferencesService.autoConfirmFingerPrints.set(true);
const value = await firstValueFrom(
organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
);
expect(value).toEqual(true);
});
it("can be unset", async () => {
await organizationManagementPreferencesService.autoConfirmFingerPrints.set(true);
await organizationManagementPreferencesService.autoConfirmFingerPrints.set(false);
const value = await firstValueFrom(
organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
);
expect(value).toEqual(false);
});
});
});

View File

@@ -0,0 +1,71 @@
import { map } from "rxjs";
import { Jsonify } from "type-fest";
import {
ORGANIZATION_MANAGEMENT_PREFERENCES_DISK,
StateProvider,
UserKeyDefinition,
} from "../../../platform/state";
import {
OrganizationManagementPreference,
OrganizationManagementPreferencesService,
} from "../../abstractions/organization-management-preferences/organization-management-preferences.service";
/**
* This helper function can be used to quickly create `KeyDefinitions` that
* target the `ORGANIZATION_MANAGEMENT_PREFERENCES_DISK` `StateDefinition`
* and that have the default deserializer and `clearOn` options. Any
* contenders for options to add to this service will likely use these same
* options.
*/
function buildKeyDefinition<T>(key: string): UserKeyDefinition<T> {
return new UserKeyDefinition<T>(ORGANIZATION_MANAGEMENT_PREFERENCES_DISK, key, {
deserializer: (obj: Jsonify<T>) => obj as T,
clearOn: ["logout"],
});
}
export const AUTO_CONFIRM_FINGERPRINTS = buildKeyDefinition<boolean>("autoConfirmFingerPrints");
export class DefaultOrganizationManagementPreferencesService
implements OrganizationManagementPreferencesService
{
constructor(private stateProvider: StateProvider) {}
autoConfirmFingerPrints = this.buildOrganizationManagementPreference(
AUTO_CONFIRM_FINGERPRINTS,
false,
);
/**
* Returns an `OrganizationManagementPreference` object for the provided
* `KeyDefinition`. This object can then be used by callers to subscribe to
* a given key, or set its value in state.
*/
private buildOrganizationManagementPreference<T>(
keyDefinition: UserKeyDefinition<T>,
defaultValue: T,
) {
return new OrganizationManagementPreference<T>(
this.getKeyFromState(keyDefinition).state$.pipe(map((x) => x ?? defaultValue)),
this.setKeyInStateFn(keyDefinition),
);
}
/**
* Returns the full `ActiveUserState` value for a given `keyDefinition`
* The returned value can then be called for subscription || set operations
*/
private getKeyFromState<T>(keyDefinition: UserKeyDefinition<T>) {
return this.stateProvider.getActive(keyDefinition);
}
/**
* Returns a function that can be called to set the given `keyDefinition` in state
*/
private setKeyInStateFn<T>(keyDefinition: UserKeyDefinition<T>) {
return async (value: T) => {
await this.getKeyFromState(keyDefinition).update(() => value);
};
}
}

Some files were not shown because too many files have changed in this diff Show More