mirror of
https://github.com/bitwarden/browser
synced 2026-02-21 03:43:58 +00:00
Merge branch 'main' into vault/PM-12423
This commit is contained in:
@@ -38,7 +38,6 @@ import {
|
||||
ProviderUserUserDetailsResponse,
|
||||
} from "../admin-console/models/response/provider/provider-user.response";
|
||||
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
|
||||
import { AuthRequest } from "../auth/models/request/auth.request";
|
||||
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
|
||||
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
|
||||
import { EmailTokenRequest } from "../auth/models/request/email-token.request";
|
||||
@@ -47,19 +46,12 @@ import { PasswordTokenRequest } from "../auth/models/request/identity-token/pass
|
||||
import { SsoTokenRequest } from "../auth/models/request/identity-token/sso-token.request";
|
||||
import { UserApiTokenRequest } from "../auth/models/request/identity-token/user-api-token.request";
|
||||
import { WebAuthnLoginTokenRequest } from "../auth/models/request/identity-token/webauthn-login-token.request";
|
||||
import { KeyConnectorUserKeyRequest } from "../auth/models/request/key-connector-user-key.request";
|
||||
import { PasswordHintRequest } from "../auth/models/request/password-hint.request";
|
||||
import { PasswordRequest } from "../auth/models/request/password.request";
|
||||
import { PasswordlessAuthRequest } from "../auth/models/request/passwordless-auth.request";
|
||||
import { SecretVerificationRequest } from "../auth/models/request/secret-verification.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../auth/models/request/set-key-connector-key.request";
|
||||
import { SetPasswordRequest } from "../auth/models/request/set-password.request";
|
||||
import { TwoFactorEmailRequest } from "../auth/models/request/two-factor-email.request";
|
||||
import { TwoFactorProviderRequest } from "../auth/models/request/two-factor-provider.request";
|
||||
import { TwoFactorRecoveryRequest } from "../auth/models/request/two-factor-recovery.request";
|
||||
import { UpdateProfileRequest } from "../auth/models/request/update-profile.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "../auth/models/request/update-tde-offboarding-password.request";
|
||||
import { UpdateTempPasswordRequest } from "../auth/models/request/update-temp-password.request";
|
||||
import { UpdateTwoFactorAuthenticatorRequest } from "../auth/models/request/update-two-factor-authenticator.request";
|
||||
import { UpdateTwoFactorDuoRequest } from "../auth/models/request/update-two-factor-duo.request";
|
||||
import { UpdateTwoFactorEmailRequest } from "../auth/models/request/update-two-factor-email.request";
|
||||
@@ -96,6 +88,8 @@ import { PaymentResponse } from "../billing/models/response/payment.response";
|
||||
import { PlanResponse } from "../billing/models/response/plan.response";
|
||||
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
|
||||
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
|
||||
import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request";
|
||||
import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
|
||||
import { EventRequest } from "../models/request/event.request";
|
||||
import { KdfRequest } from "../models/request/kdf.request";
|
||||
@@ -169,8 +163,6 @@ export abstract class ApiService {
|
||||
postPrelogin: (request: PreloginRequest) => Promise<PreloginResponse>;
|
||||
postEmailToken: (request: EmailTokenRequest) => Promise<any>;
|
||||
postEmail: (request: EmailRequest) => Promise<any>;
|
||||
postPassword: (request: PasswordRequest) => Promise<any>;
|
||||
setPassword: (request: SetPasswordRequest) => Promise<any>;
|
||||
postSetKeyConnectorKey: (request: SetKeyConnectorKeyRequest) => Promise<any>;
|
||||
postSecurityStamp: (request: SecretVerificationRequest) => Promise<any>;
|
||||
getAccountRevisionDate: () => Promise<number>;
|
||||
@@ -189,13 +181,8 @@ export abstract class ApiService {
|
||||
postAccountKdf: (request: KdfRequest) => Promise<any>;
|
||||
postUserApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
|
||||
postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
|
||||
putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise<any>;
|
||||
putUpdateTdeOffboardingPassword: (request: UpdateTdeOffboardingPasswordRequest) => Promise<any>;
|
||||
postConvertToKeyConnector: () => Promise<void>;
|
||||
//passwordless
|
||||
postAuthRequest: (request: AuthRequest) => Promise<AuthRequestResponse>;
|
||||
postAdminAuthRequest: (request: AuthRequest) => Promise<AuthRequestResponse>;
|
||||
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
|
||||
getAuthRequest: (id: string) => Promise<AuthRequestResponse>;
|
||||
putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise<AuthRequestResponse>;
|
||||
getAuthRequests: () => Promise<ListResponse<AuthRequestResponse>>;
|
||||
@@ -356,7 +343,6 @@ export abstract class ApiService {
|
||||
organizationId: string,
|
||||
request: TwoFactorProviderRequest,
|
||||
) => Promise<TwoFactorProviderResponse>;
|
||||
postTwoFactorRecover: (request: TwoFactorRecoveryRequest) => Promise<any>;
|
||||
postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise<any>;
|
||||
postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise<any>;
|
||||
getDeviceVerificationSettings: () => Promise<DeviceVerificationResponse>;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
import { PolicyType } from "../../enums";
|
||||
import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options";
|
||||
@@ -7,19 +5,23 @@ import { Policy } from "../../models/domain/policy";
|
||||
import { PolicyRequest } from "../../models/request/policy.request";
|
||||
import { PolicyResponse } from "../../models/response/policy.response";
|
||||
|
||||
export class PolicyApiServiceAbstraction {
|
||||
getPolicy: (organizationId: string, type: PolicyType) => Promise<PolicyResponse>;
|
||||
getPolicies: (organizationId: string) => Promise<ListResponse<PolicyResponse>>;
|
||||
export abstract class PolicyApiServiceAbstraction {
|
||||
abstract getPolicy: (organizationId: string, type: PolicyType) => Promise<PolicyResponse>;
|
||||
abstract getPolicies: (organizationId: string) => Promise<ListResponse<PolicyResponse>>;
|
||||
|
||||
getPoliciesByToken: (
|
||||
abstract getPoliciesByToken: (
|
||||
organizationId: string,
|
||||
token: string,
|
||||
email: string,
|
||||
organizationUserId: string,
|
||||
) => Promise<Policy[] | undefined>;
|
||||
|
||||
getMasterPasswordPolicyOptsForOrgUser: (
|
||||
abstract getMasterPasswordPolicyOptsForOrgUser: (
|
||||
orgId: string,
|
||||
) => Promise<MasterPasswordPolicyOptions | null>;
|
||||
putPolicy: (organizationId: string, type: PolicyType, request: PolicyRequest) => Promise<any>;
|
||||
abstract putPolicy: (
|
||||
organizationId: string,
|
||||
type: PolicyType,
|
||||
request: PolicyRequest,
|
||||
) => Promise<any>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "../../../types/guid";
|
||||
@@ -11,43 +9,27 @@ import { ResetPasswordPolicyOptions } from "../../models/domain/reset-password-p
|
||||
|
||||
export abstract class PolicyService {
|
||||
/**
|
||||
* All policies for the active user from sync data.
|
||||
* All policies for the provided user from sync data.
|
||||
* May include policies that are disabled or otherwise do not apply to the user. Be careful using this!
|
||||
* Consider using {@link get$} or {@link getAll$} instead, which will only return policies that should be enforced against the user.
|
||||
* Consider {@link policiesByType$} instead, which will only return policies that should be enforced against the user.
|
||||
*/
|
||||
policies$: Observable<Policy[]>;
|
||||
abstract policies$: (userId: UserId) => Observable<Policy[]>;
|
||||
|
||||
/**
|
||||
* @returns the first {@link Policy} found that applies to the active user.
|
||||
* @returns all {@link Policy} objects of a given type that apply to the specified user.
|
||||
* A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner).
|
||||
* @param policyType the {@link PolicyType} to search for
|
||||
* @see {@link getAll$} if you need all policies of a given type
|
||||
* @param userId the {@link UserId} to search against
|
||||
*/
|
||||
get$: (policyType: PolicyType) => Observable<Policy>;
|
||||
abstract policiesByType$: (policyType: PolicyType, userId: UserId) => Observable<Policy[]>;
|
||||
|
||||
/**
|
||||
* @returns all {@link Policy} objects of a given type that apply to the specified user (or the active user if not specified).
|
||||
* @returns true if a policy of the specified type applies to the specified user, otherwise false.
|
||||
* A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner).
|
||||
* @param policyType the {@link PolicyType} to search for
|
||||
*/
|
||||
getAll$: (policyType: PolicyType, userId: UserId) => Observable<Policy[]>;
|
||||
|
||||
/**
|
||||
* All {@link Policy} objects for the specified user (from sync data).
|
||||
* May include policies that are disabled or otherwise do not apply to the user.
|
||||
* Consider using {@link getAll$} instead, which will only return policies that should be enforced against the user.
|
||||
*/
|
||||
getAll: (policyType: PolicyType) => Promise<Policy[]>;
|
||||
|
||||
/**
|
||||
* @returns true if a policy of the specified type applies to the active user, otherwise false.
|
||||
* A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner).
|
||||
* This does not take into account the policy's configuration - if that is important, use {@link getAll$} to get the
|
||||
* This does not take into account the policy's configuration - if that is important, use {@link policiesByType$} to get the
|
||||
* {@link Policy} objects and then filter by Policy.data.
|
||||
*/
|
||||
policyAppliesToActiveUser$: (policyType: PolicyType) => Observable<boolean>;
|
||||
|
||||
policyAppliesToUser: (policyType: PolicyType) => Promise<boolean>;
|
||||
abstract policyAppliesToUser$: (policyType: PolicyType, userId: UserId) => Observable<boolean>;
|
||||
|
||||
// Policy specific interfaces
|
||||
|
||||
@@ -56,28 +38,31 @@ export abstract class PolicyService {
|
||||
* @returns a set of options which represent the minimum Master Password settings that the user must
|
||||
* comply with in order to comply with **all** Master Password policies.
|
||||
*/
|
||||
masterPasswordPolicyOptions$: (policies?: Policy[]) => Observable<MasterPasswordPolicyOptions>;
|
||||
abstract masterPasswordPolicyOptions$: (
|
||||
userId: UserId,
|
||||
policies?: Policy[],
|
||||
) => Observable<MasterPasswordPolicyOptions | undefined>;
|
||||
|
||||
/**
|
||||
* Evaluates whether a proposed Master Password complies with all Master Password policies that apply to the user.
|
||||
*/
|
||||
evaluateMasterPassword: (
|
||||
abstract evaluateMasterPassword: (
|
||||
passwordStrength: number,
|
||||
newPassword: string,
|
||||
enforcedPolicyOptions?: MasterPasswordPolicyOptions,
|
||||
) => boolean;
|
||||
|
||||
/**
|
||||
* @returns Reset Password policy options for the specified organization and a boolean indicating whether the policy
|
||||
* @returns {@link ResetPasswordPolicyOptions} for the specified organization and a boolean indicating whether the policy
|
||||
* is enabled
|
||||
*/
|
||||
getResetPasswordPolicyOptions: (
|
||||
abstract getResetPasswordPolicyOptions: (
|
||||
policies: Policy[],
|
||||
orgId: string,
|
||||
) => [ResetPasswordPolicyOptions, boolean];
|
||||
}
|
||||
|
||||
export abstract class InternalPolicyService extends PolicyService {
|
||||
upsert: (policy: PolicyData) => Promise<void>;
|
||||
replace: (policies: { [id: string]: PolicyData }, userId: UserId) => Promise<void>;
|
||||
abstract upsert: (policy: PolicyData, userId: UserId) => Promise<void>;
|
||||
abstract replace: (policies: { [id: string]: PolicyData }, userId: UserId) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { PolicyType } from "../../enums";
|
||||
import { PolicyData } from "../../models/data/policy.data";
|
||||
import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options";
|
||||
import { Policy } from "../../models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "../../models/domain/reset-password-policy-options";
|
||||
|
||||
export abstract class vNextPolicyService {
|
||||
/**
|
||||
* All policies for the provided user from sync data.
|
||||
* May include policies that are disabled or otherwise do not apply to the user. Be careful using this!
|
||||
* Consider {@link policiesByType$} instead, which will only return policies that should be enforced against the user.
|
||||
*/
|
||||
abstract policies$: (userId: UserId) => Observable<Policy[]>;
|
||||
|
||||
/**
|
||||
* @returns all {@link Policy} objects of a given type that apply to the specified user.
|
||||
* A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner).
|
||||
* @param policyType the {@link PolicyType} to search for
|
||||
* @param userId the {@link UserId} to search against
|
||||
*/
|
||||
abstract policiesByType$: (policyType: PolicyType, userId: UserId) => Observable<Policy[]>;
|
||||
|
||||
/**
|
||||
* @returns true if a policy of the specified type applies to the specified user, otherwise false.
|
||||
* A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner).
|
||||
* This does not take into account the policy's configuration - if that is important, use {@link policiesByType$} to get the
|
||||
* {@link Policy} objects and then filter by Policy.data.
|
||||
*/
|
||||
abstract policyAppliesToUser$: (policyType: PolicyType, userId: UserId) => Observable<boolean>;
|
||||
|
||||
// Policy specific interfaces
|
||||
|
||||
/**
|
||||
* Combines all Master Password policies that apply to the user.
|
||||
* @returns a set of options which represent the minimum Master Password settings that the user must
|
||||
* comply with in order to comply with **all** Master Password policies.
|
||||
*/
|
||||
abstract masterPasswordPolicyOptions$: (
|
||||
userId: UserId,
|
||||
policies?: Policy[],
|
||||
) => Observable<MasterPasswordPolicyOptions | undefined>;
|
||||
|
||||
/**
|
||||
* Evaluates whether a proposed Master Password complies with all Master Password policies that apply to the user.
|
||||
*/
|
||||
abstract evaluateMasterPassword: (
|
||||
passwordStrength: number,
|
||||
newPassword: string,
|
||||
enforcedPolicyOptions?: MasterPasswordPolicyOptions,
|
||||
) => boolean;
|
||||
|
||||
/**
|
||||
* @returns {@link ResetPasswordPolicyOptions} for the specified organization and a boolean indicating whether the policy
|
||||
* is enabled
|
||||
*/
|
||||
abstract getResetPasswordPolicyOptions: (
|
||||
policies: Policy[],
|
||||
orgId: string,
|
||||
) => [ResetPasswordPolicyOptions, boolean];
|
||||
}
|
||||
|
||||
export abstract class vNextInternalPolicyService extends vNextPolicyService {
|
||||
abstract upsert: (policy: PolicyData, userId: UserId) => Promise<void>;
|
||||
abstract replace: (policies: { [id: string]: PolicyData }, userId: UserId) => Promise<void>;
|
||||
}
|
||||
@@ -15,11 +15,11 @@ import { MasterPasswordPolicyOptions } from "../../../admin-console/models/domai
|
||||
import { Organization } from "../../../admin-console/models/domain/organization";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "../../../admin-console/models/domain/reset-password-policy-options";
|
||||
import { POLICIES } from "../../../admin-console/services/policy/policy.service";
|
||||
import { PolicyId, UserId } from "../../../types/guid";
|
||||
import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction";
|
||||
|
||||
import { DefaultvNextPolicyService, getFirstPolicy } from "./default-vnext-policy.service";
|
||||
import { DefaultPolicyService, getFirstPolicy } from "./default-policy.service";
|
||||
import { POLICIES } from "./policy-state";
|
||||
|
||||
describe("PolicyService", () => {
|
||||
const userId = "userId" as UserId;
|
||||
@@ -27,7 +27,7 @@ describe("PolicyService", () => {
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
let singleUserState: FakeSingleUserState<Record<PolicyId, PolicyData>>;
|
||||
|
||||
let policyService: DefaultvNextPolicyService;
|
||||
let policyService: DefaultPolicyService;
|
||||
|
||||
beforeEach(() => {
|
||||
const accountService = mockAccountServiceWith(userId);
|
||||
@@ -59,7 +59,7 @@ describe("PolicyService", () => {
|
||||
|
||||
organizationService.organizations$.calledWith(userId).mockReturnValue(organizations$);
|
||||
|
||||
policyService = new DefaultvNextPolicyService(stateProvider, organizationService);
|
||||
policyService = new DefaultPolicyService(stateProvider, organizationService);
|
||||
});
|
||||
|
||||
it("upsert", async () => {
|
||||
@@ -3,7 +3,7 @@ import { combineLatest, map, Observable, of } from "rxjs";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction";
|
||||
import { vNextPolicyService } from "../../abstractions/policy/vnext-policy.service";
|
||||
import { PolicyService } from "../../abstractions/policy/policy.service.abstraction";
|
||||
import { OrganizationUserStatusType, PolicyType } from "../../enums";
|
||||
import { PolicyData } from "../../models/data/policy.data";
|
||||
import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options";
|
||||
@@ -11,7 +11,7 @@ import { Organization } from "../../models/domain/organization";
|
||||
import { Policy } from "../../models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "../../models/domain/reset-password-policy-options";
|
||||
|
||||
import { POLICIES } from "./vnext-policy-state";
|
||||
import { POLICIES } from "./policy-state";
|
||||
|
||||
export function policyRecordToArray(policiesMap: { [id: string]: PolicyData }) {
|
||||
return Object.values(policiesMap || {}).map((f) => new Policy(f));
|
||||
@@ -21,7 +21,7 @@ export const getFirstPolicy = map<Policy[], Policy | undefined>((policies) => {
|
||||
return policies.at(0) ?? undefined;
|
||||
});
|
||||
|
||||
export class DefaultvNextPolicyService implements vNextPolicyService {
|
||||
export class DefaultPolicyService implements PolicyService {
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private organizationService: OrganizationService,
|
||||
@@ -89,7 +89,7 @@ export class DefaultvNextPolicyService implements vNextPolicyService {
|
||||
const policies$ = policies ? of(policies) : this.policies$(userId);
|
||||
return policies$.pipe(
|
||||
map((obsPolicies) => {
|
||||
const enforcedOptions: MasterPasswordPolicyOptions = new MasterPasswordPolicyOptions();
|
||||
let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined;
|
||||
const filteredPolicies =
|
||||
obsPolicies.filter((p) => p.type === PolicyType.MasterPassword) ?? [];
|
||||
|
||||
@@ -102,6 +102,10 @@ export class DefaultvNextPolicyService implements vNextPolicyService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enforcedOptions) {
|
||||
enforcedOptions = new MasterPasswordPolicyOptions();
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minComplexity != null &&
|
||||
currentPolicy.data.minComplexity > enforcedOptions.minComplexity
|
||||
@@ -1,6 +1,8 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, map, switchMap } from "rxjs";
|
||||
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { getUserId } from "../../../auth/services/account.service";
|
||||
import { HttpStatusCode } from "../../../enums";
|
||||
import { ErrorResponse } from "../../../models/response/error.response";
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
@@ -18,6 +20,7 @@ export class PolicyApiService implements PolicyApiServiceAbstraction {
|
||||
constructor(
|
||||
private policyService: InternalPolicyService,
|
||||
private apiService: ApiService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async getPolicy(organizationId: string, type: PolicyType): Promise<PolicyResponse> {
|
||||
@@ -93,8 +96,14 @@ export class PolicyApiService implements PolicyApiServiceAbstraction {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$([masterPasswordPolicy]),
|
||||
return firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.masterPasswordPolicyOptions$(userId, [masterPasswordPolicy]),
|
||||
),
|
||||
map((policy) => policy ?? null),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
// If policy not found, return null
|
||||
@@ -114,8 +123,9 @@ export class PolicyApiService implements PolicyApiServiceAbstraction {
|
||||
true,
|
||||
true,
|
||||
);
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const response = new PolicyResponse(r);
|
||||
const data = new PolicyData(response);
|
||||
await this.policyService.upsert(data);
|
||||
await this.policyService.upsert(data, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,556 +0,0 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
|
||||
import { FakeActiveUserState, FakeSingleUserState } from "../../../../spec/fake-state";
|
||||
import {
|
||||
OrganizationUserStatusType,
|
||||
OrganizationUserType,
|
||||
PolicyType,
|
||||
} from "../../../admin-console/enums";
|
||||
import { PermissionsApi } from "../../../admin-console/models/api/permissions.api";
|
||||
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
|
||||
import { PolicyData } from "../../../admin-console/models/data/policy.data";
|
||||
import { MasterPasswordPolicyOptions } from "../../../admin-console/models/domain/master-password-policy-options";
|
||||
import { Organization } from "../../../admin-console/models/domain/organization";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "../../../admin-console/models/domain/reset-password-policy-options";
|
||||
import { POLICIES, PolicyService } from "../../../admin-console/services/policy/policy.service";
|
||||
import { PolicyId, UserId } from "../../../types/guid";
|
||||
import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction";
|
||||
|
||||
describe("PolicyService", () => {
|
||||
const userId = "userId" as UserId;
|
||||
let stateProvider: FakeStateProvider;
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
let activeUserState: FakeActiveUserState<Record<PolicyId, PolicyData>>;
|
||||
let singleUserState: FakeSingleUserState<Record<PolicyId, PolicyData>>;
|
||||
|
||||
let policyService: PolicyService;
|
||||
|
||||
beforeEach(() => {
|
||||
const accountService = mockAccountServiceWith(userId);
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
organizationService = mock<OrganizationService>();
|
||||
|
||||
activeUserState = stateProvider.activeUser.getFake(POLICIES);
|
||||
singleUserState = stateProvider.singleUser.getFake(activeUserState.userId, POLICIES);
|
||||
|
||||
const organizations$ = of([
|
||||
// User
|
||||
organization("org1", true, true, OrganizationUserStatusType.Confirmed, false),
|
||||
// Owner
|
||||
organization(
|
||||
"org2",
|
||||
true,
|
||||
true,
|
||||
OrganizationUserStatusType.Confirmed,
|
||||
false,
|
||||
OrganizationUserType.Owner,
|
||||
),
|
||||
// Does not use policies
|
||||
organization("org3", true, false, OrganizationUserStatusType.Confirmed, false),
|
||||
// Another User
|
||||
organization("org4", true, true, OrganizationUserStatusType.Confirmed, false),
|
||||
// Another User
|
||||
organization("org5", true, true, OrganizationUserStatusType.Confirmed, false),
|
||||
// Can manage policies
|
||||
organization("org6", true, true, OrganizationUserStatusType.Confirmed, true),
|
||||
]);
|
||||
|
||||
organizationService.organizations$.mockReturnValue(organizations$);
|
||||
|
||||
policyService = new PolicyService(stateProvider, organizationService);
|
||||
});
|
||||
|
||||
it("upsert", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, { minutes: 14 }),
|
||||
]),
|
||||
);
|
||||
|
||||
await policyService.upsert(policyData("99", "test-organization", PolicyType.DisableSend, true));
|
||||
|
||||
expect(await firstValueFrom(policyService.policies$)).toEqual([
|
||||
{
|
||||
id: "1",
|
||||
organizationId: "test-organization",
|
||||
type: PolicyType.MaximumVaultTimeout,
|
||||
enabled: true,
|
||||
data: { minutes: 14 },
|
||||
},
|
||||
{
|
||||
id: "99",
|
||||
organizationId: "test-organization",
|
||||
type: PolicyType.DisableSend,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("replace", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, { minutes: 14 }),
|
||||
]),
|
||||
);
|
||||
|
||||
await policyService.replace(
|
||||
{
|
||||
"2": policyData("2", "test-organization", PolicyType.DisableSend, true),
|
||||
},
|
||||
userId,
|
||||
);
|
||||
|
||||
expect(await firstValueFrom(policyService.policies$)).toEqual([
|
||||
{
|
||||
id: "2",
|
||||
organizationId: "test-organization",
|
||||
type: PolicyType.DisableSend,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe("masterPasswordPolicyOptions", () => {
|
||||
it("returns default policy options", async () => {
|
||||
const data: any = {
|
||||
minComplexity: 5,
|
||||
minLength: 20,
|
||||
requireUpper: true,
|
||||
};
|
||||
const model = [
|
||||
new Policy(policyData("1", "test-organization-3", PolicyType.MasterPassword, true, data)),
|
||||
];
|
||||
const result = await firstValueFrom(policyService.masterPasswordPolicyOptions$(model));
|
||||
|
||||
expect(result).toEqual({
|
||||
minComplexity: 5,
|
||||
minLength: 20,
|
||||
requireLower: false,
|
||||
requireNumbers: false,
|
||||
requireSpecial: false,
|
||||
requireUpper: true,
|
||||
enforceOnLogin: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null", async () => {
|
||||
const data: any = {};
|
||||
const model = [
|
||||
new Policy(
|
||||
policyData("3", "test-organization-3", PolicyType.DisablePersonalVaultExport, true, data),
|
||||
),
|
||||
new Policy(
|
||||
policyData("4", "test-organization-3", PolicyType.MaximumVaultTimeout, true, data),
|
||||
),
|
||||
];
|
||||
|
||||
const result = await firstValueFrom(policyService.masterPasswordPolicyOptions$(model));
|
||||
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
|
||||
it("returns specified policy options", async () => {
|
||||
const data: any = {
|
||||
minLength: 14,
|
||||
};
|
||||
const model = [
|
||||
new Policy(
|
||||
policyData("3", "test-organization-3", PolicyType.DisablePersonalVaultExport, true, data),
|
||||
),
|
||||
new Policy(policyData("4", "test-organization-3", PolicyType.MasterPassword, true, data)),
|
||||
];
|
||||
|
||||
const result = await firstValueFrom(policyService.masterPasswordPolicyOptions$(model));
|
||||
|
||||
expect(result).toEqual({
|
||||
minComplexity: 0,
|
||||
minLength: 14,
|
||||
requireLower: false,
|
||||
requireNumbers: false,
|
||||
requireSpecial: false,
|
||||
requireUpper: false,
|
||||
enforceOnLogin: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("evaluateMasterPassword", () => {
|
||||
it("false", async () => {
|
||||
const enforcedPolicyOptions = new MasterPasswordPolicyOptions();
|
||||
enforcedPolicyOptions.minLength = 14;
|
||||
const result = policyService.evaluateMasterPassword(10, "password", enforcedPolicyOptions);
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("true", async () => {
|
||||
const enforcedPolicyOptions = new MasterPasswordPolicyOptions();
|
||||
const result = policyService.evaluateMasterPassword(0, "password", enforcedPolicyOptions);
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getResetPasswordPolicyOptions", () => {
|
||||
it("default", async () => {
|
||||
const result = policyService.getResetPasswordPolicyOptions([], "");
|
||||
|
||||
expect(result).toEqual([new ResetPasswordPolicyOptions(), false]);
|
||||
});
|
||||
|
||||
it("returns autoEnrollEnabled true", async () => {
|
||||
const data: any = {
|
||||
autoEnrollEnabled: true,
|
||||
};
|
||||
const policies = [
|
||||
new Policy(policyData("5", "test-organization-3", PolicyType.ResetPassword, true, data)),
|
||||
];
|
||||
const result = policyService.getResetPasswordPolicyOptions(policies, "test-organization-3");
|
||||
|
||||
expect(result).toEqual([{ autoEnrollEnabled: true }, true]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("get$", () => {
|
||||
it("returns the specified PolicyType", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy2", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy3", "org1", PolicyType.RemoveUnlockWithPin, true),
|
||||
]),
|
||||
);
|
||||
|
||||
await expect(
|
||||
firstValueFrom(policyService.get$(PolicyType.ActivateAutofill)),
|
||||
).resolves.toMatchObject({
|
||||
id: "policy1",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.ActivateAutofill,
|
||||
enabled: true,
|
||||
});
|
||||
await expect(
|
||||
firstValueFrom(policyService.get$(PolicyType.DisablePersonalVaultExport)),
|
||||
).resolves.toMatchObject({
|
||||
id: "policy2",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
});
|
||||
await expect(
|
||||
firstValueFrom(policyService.get$(PolicyType.RemoveUnlockWithPin)),
|
||||
).resolves.toMatchObject({
|
||||
id: "policy3",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.RemoveUnlockWithPin,
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not return disabled policies", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy2", "org1", PolicyType.DisablePersonalVaultExport, false),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.get$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("does not return policies that do not apply to the user because the user's role is exempt", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy2", "org2", PolicyType.DisablePersonalVaultExport, false),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.get$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["owners", "org2"],
|
||||
["administrators", "org6"],
|
||||
])("returns the password generator policy for %s", async (_, organization) => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org1", PolicyType.ActivateAutofill, false),
|
||||
policyData("policy2", organization, PolicyType.PasswordGenerator, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(policyService.get$(PolicyType.PasswordGenerator));
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not return policies for organizations that do not use policies", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org3", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy2", "org2", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(policyService.get$(PolicyType.ActivateAutofill));
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAll$", () => {
|
||||
it("returns the specified PolicyTypes", async () => {
|
||||
singleUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId),
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "policy1",
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy3",
|
||||
organizationId: "org5",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy4",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not return disabled policies", async () => {
|
||||
singleUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, false), // disabled
|
||||
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId),
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "policy1",
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy4",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not return policies that do not apply to the user because the user's role is exempt", async () => {
|
||||
singleUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy4", "org2", PolicyType.DisablePersonalVaultExport, true), // owner
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId),
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "policy1",
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy3",
|
||||
organizationId: "org5",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not return policies for organizations that do not use policies", async () => {
|
||||
singleUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org3", PolicyType.DisablePersonalVaultExport, true), // does not use policies
|
||||
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId),
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "policy1",
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy4",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policyAppliesToActiveUser$", () => {
|
||||
it("returns true when the policyType applies to the user", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when policyType is disabled", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, false), // disabled
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when the policyType does not apply to the user because the user's role is exempt", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy4", "org2", PolicyType.DisablePersonalVaultExport, true), // owner
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for organizations that do not use policies", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org3", PolicyType.DisablePersonalVaultExport, true), // does not use policies
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function policyData(
|
||||
id: string,
|
||||
organizationId: string,
|
||||
type: PolicyType,
|
||||
enabled: boolean,
|
||||
data?: any,
|
||||
) {
|
||||
const policyData = new PolicyData({} as any);
|
||||
policyData.id = id as PolicyId;
|
||||
policyData.organizationId = organizationId;
|
||||
policyData.type = type;
|
||||
policyData.enabled = enabled;
|
||||
policyData.data = data;
|
||||
|
||||
return policyData;
|
||||
}
|
||||
|
||||
function organizationData(
|
||||
id: string,
|
||||
enabled: boolean,
|
||||
usePolicies: boolean,
|
||||
status: OrganizationUserStatusType,
|
||||
managePolicies: boolean,
|
||||
type: OrganizationUserType = OrganizationUserType.User,
|
||||
) {
|
||||
const organizationData = new OrganizationData({} as any, {} as any);
|
||||
organizationData.id = id;
|
||||
organizationData.enabled = enabled;
|
||||
organizationData.usePolicies = usePolicies;
|
||||
organizationData.status = status;
|
||||
organizationData.permissions = new PermissionsApi({ managePolicies: managePolicies } as any);
|
||||
organizationData.type = type;
|
||||
return organizationData;
|
||||
}
|
||||
|
||||
function organization(
|
||||
id: string,
|
||||
enabled: boolean,
|
||||
usePolicies: boolean,
|
||||
status: OrganizationUserStatusType,
|
||||
managePolicies: boolean,
|
||||
type: OrganizationUserType = OrganizationUserType.User,
|
||||
) {
|
||||
return new Organization(
|
||||
organizationData(id, enabled, usePolicies, status, managePolicies, type),
|
||||
);
|
||||
}
|
||||
|
||||
function arrayToRecord(input: PolicyData[]): Record<PolicyId, PolicyData> {
|
||||
return Object.fromEntries(input.map((i) => [i.id, i]));
|
||||
}
|
||||
});
|
||||
@@ -1,257 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { combineLatest, firstValueFrom, map, Observable, of, switchMap } from "rxjs";
|
||||
|
||||
import { UserKeyDefinition, POLICIES_DISK, StateProvider } from "../../../platform/state";
|
||||
import { PolicyId, UserId } from "../../../types/guid";
|
||||
import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction";
|
||||
import { InternalPolicyService as InternalPolicyServiceAbstraction } from "../../abstractions/policy/policy.service.abstraction";
|
||||
import { OrganizationUserStatusType, PolicyType } from "../../enums";
|
||||
import { PolicyData } from "../../models/data/policy.data";
|
||||
import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options";
|
||||
import { Organization } from "../../models/domain/organization";
|
||||
import { Policy } from "../../models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "../../models/domain/reset-password-policy-options";
|
||||
|
||||
const policyRecordToArray = (policiesMap: { [id: string]: PolicyData }) =>
|
||||
Object.values(policiesMap || {}).map((f) => new Policy(f));
|
||||
|
||||
export const POLICIES = UserKeyDefinition.record<PolicyData, PolicyId>(POLICIES_DISK, "policies", {
|
||||
deserializer: (policyData) => policyData,
|
||||
clearOn: ["logout"],
|
||||
});
|
||||
|
||||
export class PolicyService implements InternalPolicyServiceAbstraction {
|
||||
private activeUserPolicyState = this.stateProvider.getActive(POLICIES);
|
||||
private activeUserPolicies$ = this.activeUserPolicyState.state$.pipe(
|
||||
map((policyData) => policyRecordToArray(policyData)),
|
||||
);
|
||||
|
||||
policies$ = this.activeUserPolicies$;
|
||||
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private organizationService: OrganizationService,
|
||||
) {}
|
||||
|
||||
get$(policyType: PolicyType): Observable<Policy> {
|
||||
const filteredPolicies$ = this.activeUserPolicies$.pipe(
|
||||
map((policies) => policies.filter((p) => p.type === policyType)),
|
||||
);
|
||||
|
||||
const organizations$ = this.stateProvider.activeUserId$.pipe(
|
||||
switchMap((userId) => this.organizationService.organizations$(userId)),
|
||||
);
|
||||
|
||||
return combineLatest([filteredPolicies$, organizations$]).pipe(
|
||||
map(
|
||||
([policies, organizations]) =>
|
||||
this.enforcedPolicyFilter(policies, organizations)?.at(0) ?? null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getAll$(policyType: PolicyType, userId: UserId) {
|
||||
const filteredPolicies$ = this.stateProvider.getUserState$(POLICIES, userId).pipe(
|
||||
map((policyData) => policyRecordToArray(policyData)),
|
||||
map((policies) => policies.filter((p) => p.type === policyType)),
|
||||
);
|
||||
|
||||
return combineLatest([filteredPolicies$, this.organizationService.organizations$(userId)]).pipe(
|
||||
map(([policies, organizations]) => this.enforcedPolicyFilter(policies, organizations)),
|
||||
);
|
||||
}
|
||||
|
||||
async getAll(policyType: PolicyType) {
|
||||
return await firstValueFrom(
|
||||
this.policies$.pipe(map((policies) => policies.filter((p) => p.type === policyType))),
|
||||
);
|
||||
}
|
||||
|
||||
policyAppliesToActiveUser$(policyType: PolicyType) {
|
||||
return this.get$(policyType).pipe(map((policy) => policy != null));
|
||||
}
|
||||
|
||||
async policyAppliesToUser(policyType: PolicyType) {
|
||||
return await firstValueFrom(this.policyAppliesToActiveUser$(policyType));
|
||||
}
|
||||
|
||||
private enforcedPolicyFilter(policies: Policy[], organizations: Organization[]) {
|
||||
const orgDict = Object.fromEntries(organizations.map((o) => [o.id, o]));
|
||||
return policies.filter((policy) => {
|
||||
const organization = orgDict[policy.organizationId];
|
||||
|
||||
// This shouldn't happen, i.e. the user should only have policies for orgs they are a member of
|
||||
// But if it does, err on the side of enforcing the policy
|
||||
if (organization == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
policy.enabled &&
|
||||
organization.status >= OrganizationUserStatusType.Accepted &&
|
||||
organization.usePolicies &&
|
||||
!this.isExemptFromPolicy(policy.type, organization)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
masterPasswordPolicyOptions$(policies?: Policy[]): Observable<MasterPasswordPolicyOptions> {
|
||||
const observable = policies ? of(policies) : this.policies$;
|
||||
return observable.pipe(
|
||||
map((obsPolicies) => {
|
||||
let enforcedOptions: MasterPasswordPolicyOptions = null;
|
||||
const filteredPolicies = obsPolicies.filter((p) => p.type === PolicyType.MasterPassword);
|
||||
|
||||
if (filteredPolicies == null || filteredPolicies.length === 0) {
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
filteredPolicies.forEach((currentPolicy) => {
|
||||
if (!currentPolicy.enabled || currentPolicy.data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enforcedOptions == null) {
|
||||
enforcedOptions = new MasterPasswordPolicyOptions();
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minComplexity != null &&
|
||||
currentPolicy.data.minComplexity > enforcedOptions.minComplexity
|
||||
) {
|
||||
enforcedOptions.minComplexity = currentPolicy.data.minComplexity;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minLength != null &&
|
||||
currentPolicy.data.minLength > enforcedOptions.minLength
|
||||
) {
|
||||
enforcedOptions.minLength = currentPolicy.data.minLength;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.requireUpper) {
|
||||
enforcedOptions.requireUpper = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.requireLower) {
|
||||
enforcedOptions.requireLower = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.requireNumbers) {
|
||||
enforcedOptions.requireNumbers = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.requireSpecial) {
|
||||
enforcedOptions.requireSpecial = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.enforceOnLogin) {
|
||||
enforcedOptions.enforceOnLogin = true;
|
||||
}
|
||||
});
|
||||
|
||||
return enforcedOptions;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
evaluateMasterPassword(
|
||||
passwordStrength: number,
|
||||
newPassword: string,
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions,
|
||||
): boolean {
|
||||
if (enforcedPolicyOptions == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
enforcedPolicyOptions.minComplexity > 0 &&
|
||||
enforcedPolicyOptions.minComplexity > passwordStrength
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
enforcedPolicyOptions.minLength > 0 &&
|
||||
enforcedPolicyOptions.minLength > newPassword.length
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.requireUpper && newPassword.toLocaleLowerCase() === newPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.requireLower && newPassword.toLocaleUpperCase() === newPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.requireNumbers && !/[0-9]/.test(newPassword)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
if (enforcedPolicyOptions.requireSpecial && !/[!@#$%\^&*]/g.test(newPassword)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getResetPasswordPolicyOptions(
|
||||
policies: Policy[],
|
||||
orgId: string,
|
||||
): [ResetPasswordPolicyOptions, boolean] {
|
||||
const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions();
|
||||
|
||||
if (policies == null || orgId == null) {
|
||||
return [resetPasswordPolicyOptions, false];
|
||||
}
|
||||
|
||||
const policy = policies.find(
|
||||
(p) => p.organizationId === orgId && p.type === PolicyType.ResetPassword && p.enabled,
|
||||
);
|
||||
resetPasswordPolicyOptions.autoEnrollEnabled = policy?.data?.autoEnrollEnabled ?? false;
|
||||
|
||||
return [resetPasswordPolicyOptions, policy?.enabled ?? false];
|
||||
}
|
||||
|
||||
async upsert(policy: PolicyData): Promise<void> {
|
||||
await this.activeUserPolicyState.update((policies) => {
|
||||
policies ??= {};
|
||||
policies[policy.id] = policy;
|
||||
return policies;
|
||||
});
|
||||
}
|
||||
|
||||
async replace(policies: { [id: string]: PolicyData }, userId: UserId): Promise<void> {
|
||||
await this.stateProvider.setUserState(POLICIES, policies, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether an orgUser is exempt from a specific policy because of their role
|
||||
* Generally orgUsers who can manage policies are exempt from them, but some policies are stricter
|
||||
*/
|
||||
private isExemptFromPolicy(policyType: PolicyType, organization: Organization) {
|
||||
switch (policyType) {
|
||||
case PolicyType.MaximumVaultTimeout:
|
||||
// Max Vault Timeout applies to everyone except owners
|
||||
return organization.isOwner;
|
||||
case PolicyType.PasswordGenerator:
|
||||
// password generation policy applies to everyone
|
||||
return false;
|
||||
case PolicyType.PersonalOwnership:
|
||||
// individual vault policy applies to everyone except admins and owners
|
||||
return organization.isAdmin;
|
||||
case PolicyType.FreeFamiliesSponsorshipPolicy:
|
||||
// free Bitwarden families policy applies to everyone
|
||||
return false;
|
||||
case PolicyType.RemoveUnlockWithPin:
|
||||
// free Remove Unlock with PIN policy applies to everyone
|
||||
return false;
|
||||
default:
|
||||
return organization.canManagePolicies;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { PasswordRequest } from "../models/request/password.request";
|
||||
import { SetPasswordRequest } from "../models/request/set-password.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "../models/request/update-tde-offboarding-password.request";
|
||||
import { UpdateTempPasswordRequest } from "../models/request/update-temp-password.request";
|
||||
|
||||
export abstract class MasterPasswordApiService {
|
||||
/**
|
||||
* POSTs a SetPasswordRequest to "/accounts/set-password"
|
||||
*/
|
||||
abstract setPassword: (request: SetPasswordRequest) => Promise<any>;
|
||||
|
||||
/**
|
||||
* POSTs a PasswordRequest to "/accounts/password"
|
||||
*/
|
||||
abstract postPassword: (request: PasswordRequest) => Promise<any>;
|
||||
|
||||
/**
|
||||
* PUTs an UpdateTempPasswordRequest to "/accounts/update-temp-password"
|
||||
*/
|
||||
abstract putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise<any>;
|
||||
|
||||
/**
|
||||
* PUTs an UpdateTdeOffboardingPasswordRequest to "/accounts/update-tde-offboarding-password"
|
||||
*/
|
||||
abstract putUpdateTdeOffboardingPassword: (
|
||||
request: UpdateTdeOffboardingPasswordRequest,
|
||||
) => Promise<any>;
|
||||
}
|
||||
@@ -14,7 +14,6 @@ export abstract class TokenRequest {
|
||||
this.device = device != null ? device : null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
alterIdentityTokenHeaders(headers: Headers) {
|
||||
// Implemented in subclass if required
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { SecretVerificationRequest } from "./secret-verification.request";
|
||||
|
||||
export class TwoFactorRecoveryRequest extends SecretVerificationRequest {
|
||||
recoveryCode: string;
|
||||
email: string;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export class AuthRequestResponse extends BaseResponse {
|
||||
requestDeviceTypeValue: DeviceType;
|
||||
requestDeviceIdentifier: string;
|
||||
requestIpAddress: string;
|
||||
requestCountryName: string;
|
||||
key: string; // could be either an encrypted MasterKey or an encrypted UserKey
|
||||
masterPasswordHash: string; // if hash is present, the `key` above is an encrypted MasterKey (else `key` is an encrypted UserKey)
|
||||
creationDate: string;
|
||||
@@ -26,6 +27,7 @@ export class AuthRequestResponse extends BaseResponse {
|
||||
this.requestDeviceTypeValue = this.getResponseProperty("RequestDeviceTypeValue");
|
||||
this.requestDeviceIdentifier = this.getResponseProperty("RequestDeviceIdentifier");
|
||||
this.requestIpAddress = this.getResponseProperty("RequestIpAddress");
|
||||
this.requestCountryName = this.getResponseProperty("RequestCountryName");
|
||||
this.key = this.getResponseProperty("Key");
|
||||
this.masterPasswordHash = this.getResponseProperty("MasterPasswordHash");
|
||||
this.creationDate = this.getResponseProperty("CreationDate");
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { View } from "@bitwarden/common/models/view/view";
|
||||
|
||||
export class LoginViaAuthRequestView implements View {
|
||||
authRequest: AuthRequest | undefined = undefined;
|
||||
authRequestResponse: AuthRequestResponse | undefined = undefined;
|
||||
fingerprintPhrase: string | undefined = undefined;
|
||||
privateKey: string | undefined = undefined;
|
||||
publicKey: string | undefined = undefined;
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<LoginViaAuthRequestView>>): LoginViaAuthRequestView {
|
||||
return Object.assign(new LoginViaAuthRequestView(), obj);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
import { MasterPasswordApiService as MasterPasswordApiServiceAbstraction } from "../../abstractions/master-password-api.service.abstraction";
|
||||
import { PasswordRequest } from "../../models/request/password.request";
|
||||
import { SetPasswordRequest } from "../../models/request/set-password.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "../../models/request/update-tde-offboarding-password.request";
|
||||
import { UpdateTempPasswordRequest } from "../../models/request/update-temp-password.request";
|
||||
|
||||
export class MasterPasswordApiService implements MasterPasswordApiServiceAbstraction {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async setPassword(request: SetPasswordRequest): Promise<any> {
|
||||
try {
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
"/accounts/set-password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
this.logService.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async postPassword(request: PasswordRequest): Promise<any> {
|
||||
try {
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
"/accounts/password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
this.logService.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async putUpdateTempPassword(request: UpdateTempPasswordRequest): Promise<any> {
|
||||
try {
|
||||
const response = await this.apiService.send(
|
||||
"PUT",
|
||||
"/accounts/update-temp-password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
this.logService.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async putUpdateTdeOffboardingPassword(
|
||||
request: UpdateTdeOffboardingPasswordRequest,
|
||||
): Promise<any> {
|
||||
try {
|
||||
const response = await this.apiService.send(
|
||||
"PUT",
|
||||
"/accounts/update-tde-offboarding-password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
this.logService.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { KdfType } from "@bitwarden/key-management";
|
||||
|
||||
import { PasswordRequest } from "../../models/request/password.request";
|
||||
import { SetPasswordRequest } from "../../models/request/set-password.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "../../models/request/update-tde-offboarding-password.request";
|
||||
import { UpdateTempPasswordRequest } from "../../models/request/update-temp-password.request";
|
||||
|
||||
import { MasterPasswordApiService } from "./master-password-api.service.implementation";
|
||||
|
||||
describe("MasterPasswordApiService", () => {
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
|
||||
let sut: MasterPasswordApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
logService = mock<LogService>();
|
||||
|
||||
sut = new MasterPasswordApiService(apiService, logService);
|
||||
});
|
||||
|
||||
it("should instantiate", () => {
|
||||
expect(sut).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("setPassword", () => {
|
||||
it("should call apiService.send with the correct parameters", async () => {
|
||||
// Arrange
|
||||
const request = new SetPasswordRequest(
|
||||
"masterPasswordHash",
|
||||
"key",
|
||||
"masterPasswordHint",
|
||||
"orgIdentifier",
|
||||
{
|
||||
publicKey: "publicKey",
|
||||
encryptedPrivateKey: "encryptedPrivateKey",
|
||||
},
|
||||
KdfType.PBKDF2_SHA256,
|
||||
600_000,
|
||||
);
|
||||
|
||||
// Act
|
||||
await sut.setPassword(request);
|
||||
|
||||
// Assert
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
"/accounts/set-password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("postPassword", () => {
|
||||
it("should call apiService.send with the correct parameters", async () => {
|
||||
// Arrange
|
||||
const request = {
|
||||
newMasterPasswordHash: "newMasterPasswordHash",
|
||||
masterPasswordHint: "masterPasswordHint",
|
||||
key: "key",
|
||||
masterPasswordHash: "masterPasswordHash",
|
||||
} as PasswordRequest;
|
||||
|
||||
// Act
|
||||
await sut.postPassword(request);
|
||||
|
||||
// Assert
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
"/accounts/password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("putUpdateTempPassword", () => {
|
||||
it("should call apiService.send with the correct parameters", async () => {
|
||||
// Arrange
|
||||
const request = {
|
||||
masterPasswordHint: "masterPasswordHint",
|
||||
newMasterPasswordHash: "newMasterPasswordHash",
|
||||
key: "key",
|
||||
} as UpdateTempPasswordRequest;
|
||||
|
||||
// Act
|
||||
await sut.putUpdateTempPassword(request);
|
||||
|
||||
// Assert
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"PUT",
|
||||
"/accounts/update-temp-password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("putUpdateTdeOffboardingPassword", () => {
|
||||
it("should call apiService.send with the correct parameters", async () => {
|
||||
// Arrange
|
||||
const request = {
|
||||
masterPasswordHint: "masterPasswordHint",
|
||||
newMasterPasswordHash: "newMasterPasswordHash",
|
||||
key: "key",
|
||||
} as UpdateTdeOffboardingPasswordRequest;
|
||||
|
||||
// Act
|
||||
await sut.putUpdateTdeOffboardingPassword(request);
|
||||
|
||||
// Assert
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"PUT",
|
||||
"/accounts/update-tde-offboarding-password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,13 +16,13 @@ import {
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../../key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "../../../key-management/vault-timeout";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { HashPurpose } from "../../../platform/enums";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey } from "../../../types/key";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { VerificationType } from "../../enums/verification-type";
|
||||
import { MasterPasswordPolicyResponse } from "../../models/response/master-password-policy.response";
|
||||
@@ -224,6 +224,8 @@ describe("UserVerificationService", () => {
|
||||
expect(result).toEqual({
|
||||
policyOptions: null,
|
||||
masterKey: "masterKey",
|
||||
kdfConfig: "kdfConfig",
|
||||
email: "email",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -282,6 +284,8 @@ describe("UserVerificationService", () => {
|
||||
expect(result).toEqual({
|
||||
policyOptions: "MasterPasswordPolicyOptions",
|
||||
masterKey: "masterKey",
|
||||
kdfConfig: "kdfConfig",
|
||||
email: "email",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../../key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { HashPurpose } from "../../../platform/enums";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { AccountService } from "../../abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "../../enums/verification-type";
|
||||
@@ -237,7 +237,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
||||
);
|
||||
await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
return { policyOptions, masterKey };
|
||||
return { policyOptions, masterKey, kdfConfig, email };
|
||||
}
|
||||
|
||||
private async verifyUserByPIN(verification: PinVerification, userId: UserId): Promise<boolean> {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { MasterKey } from "../../types/key";
|
||||
import { VerificationType } from "../enums/verification-type";
|
||||
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
|
||||
@@ -22,5 +24,7 @@ export type ServerSideVerification = OtpVerification | MasterPasswordVerificatio
|
||||
|
||||
export type MasterPasswordVerificationResponse = {
|
||||
masterKey: MasterKey;
|
||||
kdfConfig: KdfConfig;
|
||||
email: string;
|
||||
policyOptions: MasterPasswordPolicyResponse | null;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { map, Observable } from "rxjs";
|
||||
import { map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "../../admin-console/enums";
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { getUserId } from "../../auth/services/account.service";
|
||||
import {
|
||||
AUTOFILL_SETTINGS_DISK,
|
||||
AUTOFILL_SETTINGS_DISK_LOCAL,
|
||||
@@ -152,6 +154,7 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private policyService: PolicyService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.autofillOnPageLoadState = this.stateProvider.getActive(AUTOFILL_ON_PAGE_LOAD);
|
||||
this.autofillOnPageLoad$ = this.autofillOnPageLoadState.state$.pipe(map((x) => x ?? false));
|
||||
@@ -169,8 +172,11 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti
|
||||
this.autofillOnPageLoadCalloutIsDismissed$ =
|
||||
this.autofillOnPageLoadCalloutIsDismissedState.state$.pipe(map((x) => x ?? false));
|
||||
|
||||
this.activateAutofillOnPageLoadFromPolicy$ = this.policyService.policyAppliesToActiveUser$(
|
||||
PolicyType.ActivateAutofill,
|
||||
this.activateAutofillOnPageLoadFromPolicy$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policyAppliesToUser$(PolicyType.ActivateAutofill, userId),
|
||||
),
|
||||
);
|
||||
|
||||
this.autofillOnPageLoadPolicyToastHasDisplayedState = this.stateProvider.getActive(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
|
||||
|
||||
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response";
|
||||
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
||||
@@ -50,6 +52,8 @@ export abstract class BillingApiServiceAbstraction {
|
||||
|
||||
getProviderSubscription: (providerId: string) => Promise<ProviderSubscriptionResponse>;
|
||||
|
||||
getProviderTaxInformation: (providerId: string) => Promise<TaxInfoResponse>;
|
||||
|
||||
updateOrganizationPaymentMethod: (
|
||||
organizationId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
@@ -66,6 +70,11 @@ export abstract class BillingApiServiceAbstraction {
|
||||
request: UpdateClientOrganizationRequest,
|
||||
) => Promise<any>;
|
||||
|
||||
updateProviderPaymentMethod: (
|
||||
providerId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
) => Promise<void>;
|
||||
|
||||
updateProviderTaxInformation: (
|
||||
providerId: string,
|
||||
request: ExpandedTaxInfoUpdateRequest,
|
||||
@@ -76,6 +85,11 @@ export abstract class BillingApiServiceAbstraction {
|
||||
request: VerifyBankAccountRequest,
|
||||
) => Promise<void>;
|
||||
|
||||
verifyProviderBankAccount: (
|
||||
providerId: string,
|
||||
request: VerifyBankAccountRequest,
|
||||
) => Promise<void>;
|
||||
|
||||
restartSubscription: (
|
||||
organizationId: string,
|
||||
request: OrganizationCreateRequest,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response";
|
||||
|
||||
import { ProviderType } from "../../../admin-console/enums";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { PlanType, ProductTierType } from "../../enums";
|
||||
@@ -16,6 +18,7 @@ export class ProviderSubscriptionResponse extends BaseResponse {
|
||||
cancelAt?: string;
|
||||
suspension?: SubscriptionSuspensionResponse;
|
||||
providerType: ProviderType;
|
||||
paymentSource?: PaymentSourceResponse;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -38,6 +41,10 @@ export class ProviderSubscriptionResponse extends BaseResponse {
|
||||
this.suspension = new SubscriptionSuspensionResponse(suspension);
|
||||
}
|
||||
this.providerType = this.getResponseProperty("providerType");
|
||||
const paymentSource = this.getResponseProperty("paymentSource");
|
||||
if (paymentSource != null) {
|
||||
this.paymentSource = new PaymentSourceResponse(paymentSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response";
|
||||
import { ErrorResponse } from "../../models/response/error.response";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { BillingApiServiceAbstraction } from "../abstractions";
|
||||
import { PaymentMethodType } from "../enums";
|
||||
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
|
||||
@@ -23,11 +22,7 @@ import { PlanResponse } from "../models/response/plan.response";
|
||||
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
|
||||
|
||||
export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private logService: LogService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
cancelOrganizationSubscription(
|
||||
organizationId: string,
|
||||
@@ -89,14 +84,12 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
}
|
||||
|
||||
async getOrganizationPaymentMethod(organizationId: string): Promise<PaymentMethodResponse> {
|
||||
const response = await this.execute(() =>
|
||||
this.apiService.send(
|
||||
"GET",
|
||||
"/organizations/" + organizationId + "/billing/payment-method",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/organizations/" + organizationId + "/billing/payment-method",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new PaymentMethodResponse(response);
|
||||
}
|
||||
@@ -120,38 +113,49 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
async getProviderClientOrganizations(
|
||||
providerId: string,
|
||||
): Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>> {
|
||||
const response = await this.execute(() =>
|
||||
this.apiService.send("GET", "/providers/" + providerId + "/organizations", null, true, true),
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/organizations",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new ListResponse(response, ProviderOrganizationOrganizationDetailsResponse);
|
||||
}
|
||||
|
||||
async getProviderInvoices(providerId: string): Promise<InvoicesResponse> {
|
||||
const response = await this.execute(() =>
|
||||
this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/invoices",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/invoices",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new InvoicesResponse(response);
|
||||
}
|
||||
|
||||
async getProviderSubscription(providerId: string): Promise<ProviderSubscriptionResponse> {
|
||||
const response = await this.execute(() =>
|
||||
this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/subscription",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/subscription",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new ProviderSubscriptionResponse(response);
|
||||
}
|
||||
|
||||
async getProviderTaxInformation(providerId: string): Promise<TaxInfoResponse> {
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/tax-information",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new TaxInfoResponse(response);
|
||||
}
|
||||
|
||||
async updateOrganizationPaymentMethod(
|
||||
organizationId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
@@ -192,6 +196,19 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async updateProviderPaymentMethod(
|
||||
providerId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"PUT",
|
||||
"/providers/" + providerId + "/billing/payment-method",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async updateProviderTaxInformation(providerId: string, request: ExpandedTaxInfoUpdateRequest) {
|
||||
return await this.apiService.send(
|
||||
"PUT",
|
||||
@@ -215,6 +232,19 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async verifyProviderBankAccount(
|
||||
providerId: string,
|
||||
request: VerifyBankAccountRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"POST",
|
||||
"/providers/" + providerId + "/billing/payment-method/verify-bank-account",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async restartSubscription(
|
||||
organizationId: string,
|
||||
request: OrganizationCreateRequest,
|
||||
@@ -227,20 +257,4 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
private async execute(request: () => Promise<any>): Promise<any> {
|
||||
try {
|
||||
return await request();
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
if (error instanceof ErrorResponse) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: error.getSingleMessage(),
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum FeatureFlag {
|
||||
AccountDeprovisioning = "pm-10308-account-deprovisioning",
|
||||
VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint",
|
||||
LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission",
|
||||
SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility",
|
||||
|
||||
/* Autofill */
|
||||
BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain",
|
||||
@@ -17,7 +18,6 @@ export enum FeatureFlag {
|
||||
IdpAutoSubmitLogin = "idp-auto-submit-login",
|
||||
InlineMenuFieldQualification = "inline-menu-field-qualification",
|
||||
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
|
||||
InlineMenuTotp = "inline-menu-totp",
|
||||
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
|
||||
NotificationRefresh = "notification-refresh",
|
||||
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
||||
@@ -26,26 +26,31 @@ export enum FeatureFlag {
|
||||
ItemShare = "item-share",
|
||||
CriticalApps = "pm-14466-risk-insights-critical-application",
|
||||
EnableRiskInsightsNotifications = "enable-risk-insights-notifications",
|
||||
DesktopSendUIRefresh = "desktop-send-ui-refresh",
|
||||
ExportAttachments = "export-attachments",
|
||||
|
||||
ExtensionRefresh = "extension-refresh",
|
||||
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
|
||||
VaultBulkManagementAction = "vault-bulk-management-action",
|
||||
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
|
||||
SSHKeyVaultItem = "ssh-key-vault-item",
|
||||
SSHAgent = "ssh-agent",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
TrialPaymentOptional = "PM-8163-trial-payment",
|
||||
SecurityTasks = "security-tasks",
|
||||
/* Vault */
|
||||
PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge",
|
||||
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
|
||||
NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss",
|
||||
NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss",
|
||||
VaultBulkManagementAction = "vault-bulk-management-action",
|
||||
SecurityTasks = "security-tasks",
|
||||
|
||||
/* Auth */
|
||||
PM9112_DeviceApprovalPersistence = "pm-9112-device-approval-persistence",
|
||||
|
||||
UserKeyRotationV2 = "userkey-rotation-v2",
|
||||
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
|
||||
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
TrialPaymentOptional = "PM-8163-trial-payment",
|
||||
MacOsNativeCredentialSync = "macos-native-credential-sync",
|
||||
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
|
||||
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
||||
ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs",
|
||||
AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner",
|
||||
PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal",
|
||||
RecoveryCodeLogin = "pm-17128-recovery-code-login",
|
||||
PM12276_BreadcrumbEventLogs = "pm-12276-breadcrumbing-for-business-features",
|
||||
PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method",
|
||||
}
|
||||
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
@@ -64,6 +69,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.AccountDeprovisioning]: FALSE,
|
||||
[FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE,
|
||||
[FeatureFlag.LimitItemDeletion]: FALSE,
|
||||
[FeatureFlag.SsoExternalIdVisibility]: FALSE,
|
||||
|
||||
/* Autofill */
|
||||
[FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE,
|
||||
@@ -73,7 +79,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
|
||||
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
|
||||
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
|
||||
[FeatureFlag.InlineMenuTotp]: FALSE,
|
||||
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
|
||||
[FeatureFlag.NotificationRefresh]: FALSE,
|
||||
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
||||
@@ -82,26 +87,31 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.ItemShare]: FALSE,
|
||||
[FeatureFlag.CriticalApps]: FALSE,
|
||||
[FeatureFlag.EnableRiskInsightsNotifications]: FALSE,
|
||||
[FeatureFlag.DesktopSendUIRefresh]: FALSE,
|
||||
[FeatureFlag.ExportAttachments]: FALSE,
|
||||
|
||||
[FeatureFlag.ExtensionRefresh]: FALSE,
|
||||
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
|
||||
[FeatureFlag.VaultBulkManagementAction]: FALSE,
|
||||
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
|
||||
[FeatureFlag.SSHKeyVaultItem]: FALSE,
|
||||
[FeatureFlag.SSHAgent]: FALSE,
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
[FeatureFlag.TrialPaymentOptional]: FALSE,
|
||||
[FeatureFlag.SecurityTasks]: FALSE,
|
||||
/* Vault */
|
||||
[FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE,
|
||||
[FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE,
|
||||
[FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE,
|
||||
[FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE,
|
||||
[FeatureFlag.VaultBulkManagementAction]: FALSE,
|
||||
[FeatureFlag.SecurityTasks]: FALSE,
|
||||
|
||||
/* Auth */
|
||||
[FeatureFlag.PM9112_DeviceApprovalPersistence]: FALSE,
|
||||
|
||||
[FeatureFlag.UserKeyRotationV2]: FALSE,
|
||||
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
|
||||
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
[FeatureFlag.TrialPaymentOptional]: FALSE,
|
||||
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
|
||||
[FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE,
|
||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||
[FeatureFlag.ResellerManagedOrgAlert]: FALSE,
|
||||
[FeatureFlag.AccountDeprovisioningBanner]: FALSE,
|
||||
[FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE,
|
||||
[FeatureFlag.RecoveryCodeLogin]: FALSE,
|
||||
[FeatureFlag.PM12276_BreadcrumbEventLogs]: FALSE,
|
||||
[FeatureFlag.PM18794_ProviderPaymentMethod]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
@@ -24,4 +24,6 @@ export enum NotificationType {
|
||||
SyncOrganizations = 17,
|
||||
SyncOrganizationStatusChanged = 18,
|
||||
SyncOrganizationCollectionSettingChanged = 19,
|
||||
Notification = 20,
|
||||
NotificationStatus = 21,
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ export abstract class EncryptService {
|
||||
): Promise<Uint8Array | null>;
|
||||
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
|
||||
abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
|
||||
abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey;
|
||||
/**
|
||||
* @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed
|
||||
* @param items The items to decrypt
|
||||
|
||||
@@ -78,8 +78,6 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
throw new Error("No key provided for decryption.");
|
||||
}
|
||||
|
||||
key = this.resolveLegacyKey(key, encString);
|
||||
|
||||
// DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
|
||||
if (key.macKey != null && encString?.mac == null) {
|
||||
this.logService.error(
|
||||
@@ -145,8 +143,6 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
throw new Error("Nothing provided for decryption.");
|
||||
}
|
||||
|
||||
key = this.resolveLegacyKey(key, encThing);
|
||||
|
||||
// DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
|
||||
if (key.macKey != null && encThing.macBytes == null) {
|
||||
this.logService.error(
|
||||
@@ -298,19 +294,4 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
this.logService.error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform into new key for the old encrypt-then-mac scheme if required, otherwise return the current key unchanged
|
||||
* @param encThing The encrypted object (e.g. encString or encArrayBuffer) that you want to decrypt
|
||||
*/
|
||||
resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey {
|
||||
if (
|
||||
encThing.encryptionType === EncryptionType.AesCbc128_HmacSha256_B64 &&
|
||||
key.encType === EncryptionType.AesCbc256_B64
|
||||
) {
|
||||
return new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +325,25 @@ describe("EncryptService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptToUtf8", () => {
|
||||
it("throws if no key is provided", () => {
|
||||
return expect(encryptService.decryptToUtf8(null, null)).rejects.toThrow(
|
||||
"No key provided for decryption.",
|
||||
);
|
||||
});
|
||||
it("returns null if key is mac key but encstring has no mac", async () => {
|
||||
const key = new SymmetricCryptoKey(
|
||||
makeStaticByteArray(64, 0),
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
);
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
||||
|
||||
const actual = await encryptService.decryptToUtf8(encString, key);
|
||||
expect(actual).toBeNull();
|
||||
expect(logService.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("rsa", () => {
|
||||
const data = makeStaticByteArray(10, 100);
|
||||
const encryptedData = makeStaticByteArray(10, 150);
|
||||
@@ -370,17 +389,16 @@ describe("EncryptService", () => {
|
||||
return expect(encryptService.rsaDecrypt(encString, null)).rejects.toThrow("No private key");
|
||||
});
|
||||
|
||||
it.each([
|
||||
EncryptionType.AesCbc256_B64,
|
||||
EncryptionType.AesCbc128_HmacSha256_B64,
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
])("throws if encryption type is %s", async (encType) => {
|
||||
encString.encryptionType = encType;
|
||||
it.each([EncryptionType.AesCbc256_B64, EncryptionType.AesCbc256_HmacSha256_B64])(
|
||||
"throws if encryption type is %s",
|
||||
async (encType) => {
|
||||
encString.encryptionType = encType;
|
||||
|
||||
await expect(encryptService.rsaDecrypt(encString, privateKey)).rejects.toThrow(
|
||||
"Invalid encryption type",
|
||||
);
|
||||
});
|
||||
await expect(encryptService.rsaDecrypt(encString, privateKey)).rejects.toThrow(
|
||||
"Invalid encryption type",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("decrypts data with provided key", async () => {
|
||||
cryptoFunctionService.rsaDecrypt.mockResolvedValue(data);
|
||||
@@ -398,30 +416,6 @@ describe("EncryptService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveLegacyKey", () => {
|
||||
it("creates a legacy key if required", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(32), EncryptionType.AesCbc256_B64);
|
||||
const encString = mock<EncString>();
|
||||
encString.encryptionType = EncryptionType.AesCbc128_HmacSha256_B64;
|
||||
|
||||
const actual = encryptService.resolveLegacyKey(key, encString);
|
||||
|
||||
const expected = new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it("does not create a legacy key if not required", async () => {
|
||||
const encType = EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64), encType);
|
||||
const encString = mock<EncString>();
|
||||
encString.encryptionType = encType;
|
||||
|
||||
const actual = encryptService.resolveLegacyKey(key, encString);
|
||||
|
||||
expect(actual).toEqual(key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hash", () => {
|
||||
it("hashes a string and returns b64", async () => {
|
||||
cryptoFunctionService.hash.mockResolvedValue(Uint8Array.from([1, 2, 3]));
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { DeviceKey, UserKey } from "../../types/key";
|
||||
|
||||
import { DeviceResponse } from "./devices/responses/device.response";
|
||||
import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { DeviceKey, UserKey } from "../../../types/key";
|
||||
|
||||
export abstract class DeviceTrustServiceAbstraction {
|
||||
/**
|
||||
@@ -16,6 +15,12 @@ export abstract class DeviceTrustServiceAbstraction {
|
||||
*/
|
||||
supportsDeviceTrust$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Emits when a device has been trusted. This emission is specifically for the purpose of notifying
|
||||
* the consuming component to display a toast informing the user the device has been trusted.
|
||||
*/
|
||||
deviceTrusted$: Observable<void>;
|
||||
|
||||
/**
|
||||
* @description Checks if the device trust feature is supported for the given user.
|
||||
*/
|
||||
@@ -1,34 +1,34 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
import { firstValueFrom, map, Observable, Subject } from "rxjs";
|
||||
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
|
||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "../../platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { AbstractStorageService } from "../../platform/abstractions/storage.service";
|
||||
import { StorageLocation } from "../../platform/enums";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { StorageOptions } from "../../platform/models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { DEVICE_TRUST_DISK_LOCAL, StateProvider, UserKeyDefinition } from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { UserKey, DeviceKey } from "../../types/key";
|
||||
import { DeviceTrustServiceAbstraction } from "../abstractions/device-trust.service.abstraction";
|
||||
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
|
||||
import { SecretVerificationRequest } from "../models/request/secret-verification.request";
|
||||
import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response";
|
||||
import { DevicesApiServiceAbstraction } from "../../../auth/abstractions/devices-api.service.abstraction";
|
||||
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
||||
import {
|
||||
DeviceKeysUpdateRequest,
|
||||
UpdateDevicesTrustRequest,
|
||||
} from "../models/request/update-devices-trust.request";
|
||||
} from "../../../auth/models/request/update-devices-trust.request";
|
||||
import { AppIdService } from "../../../platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "../../../platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
|
||||
import { AbstractStorageService } from "../../../platform/abstractions/storage.service";
|
||||
import { StorageLocation } from "../../../platform/enums";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
import { StorageOptions } from "../../../platform/models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { DEVICE_TRUST_DISK_LOCAL, StateProvider, UserKeyDefinition } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { UserKey, DeviceKey } from "../../../types/key";
|
||||
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
|
||||
import { DeviceTrustServiceAbstraction } from "../abstractions/device-trust.service.abstraction";
|
||||
|
||||
/** Uses disk storage so that the device key can persist after log out and tab removal. */
|
||||
export const DEVICE_KEY = new UserKeyDefinition<DeviceKey | null>(
|
||||
@@ -63,6 +63,10 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
|
||||
supportsDeviceTrust$: Observable<boolean>;
|
||||
|
||||
// Observable emission is used to trigger a toast in consuming components
|
||||
private deviceTrustedSubject = new Subject<void>();
|
||||
deviceTrusted$ = this.deviceTrustedSubject.asObservable();
|
||||
|
||||
constructor(
|
||||
private keyGenerationService: KeyGenerationService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
@@ -177,7 +181,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
// store device key in local/secure storage if enc keys posted to server successfully
|
||||
await this.setDeviceKey(userId, deviceKey);
|
||||
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("deviceTrusted"));
|
||||
// This emission will be picked up by consuming components to handle displaying a toast to the user
|
||||
this.deviceTrustedSubject.next();
|
||||
|
||||
return deviceResponse;
|
||||
}
|
||||
@@ -3,38 +3,38 @@
|
||||
import { matches, mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
UserDecryptionOptions,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||
import { FakeActiveUserState } from "../../../spec/fake-state";
|
||||
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||
import { DeviceType } from "../../enums";
|
||||
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
|
||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "../../platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { AbstractStorageService } from "../../platform/abstractions/storage.service";
|
||||
import { StorageLocation } from "../../platform/enums";
|
||||
import { EncryptionType } from "../../platform/enums/encryption-type.enum";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { StorageOptions } from "../../platform/models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { DeviceKey, UserKey } from "../../types/key";
|
||||
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
|
||||
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
|
||||
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service";
|
||||
import { FakeActiveUserState } from "../../../../spec/fake-state";
|
||||
import { FakeStateProvider } from "../../../../spec/fake-state-provider";
|
||||
import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response";
|
||||
import { DevicesApiServiceAbstraction } from "../../../auth/abstractions/devices-api.service.abstraction";
|
||||
import { UpdateDevicesTrustRequest } from "../../../auth/models/request/update-devices-trust.request";
|
||||
import { ProtectedDeviceResponse } from "../../../auth/models/response/protected-device.response";
|
||||
import { DeviceType } from "../../../enums";
|
||||
import { AppIdService } from "../../../platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "../../../platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
|
||||
import { AbstractStorageService } from "../../../platform/abstractions/storage.service";
|
||||
import { StorageLocation } from "../../../platform/enums";
|
||||
import { EncryptionType } from "../../../platform/enums/encryption-type.enum";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
import { StorageOptions } from "../../../platform/models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../../types/csprng";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { DeviceKey, UserKey } from "../../../types/key";
|
||||
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
|
||||
|
||||
import {
|
||||
SHOULD_TRUST_DEVICE,
|
||||
@@ -1,8 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Organization } from "../../admin-console/models/domain/organization";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
import { Organization } from "../../../admin-console/models/domain/organization";
|
||||
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
|
||||
import { UserId } from "../../../types/guid";
|
||||
|
||||
export abstract class KeyConnectorService {
|
||||
setMasterKeyFromUrl: (url: string, userId: UserId) => Promise<void>;
|
||||
@@ -4,27 +4,27 @@ import { of } from "rxjs";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { OrganizationData } from "../../admin-console/models/data/organization.data";
|
||||
import { Organization } from "../../admin-console/models/domain/organization";
|
||||
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { KeyGenerationService } from "../../platform/services/key-generation.service";
|
||||
import { OrganizationId, UserId } from "../../types/guid";
|
||||
import { MasterKey } from "../../types/key";
|
||||
import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request";
|
||||
import { KeyConnectorUserKeyResponse } from "../models/response/key-connector-user-key.response";
|
||||
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
|
||||
import { Organization } from "../../../admin-console/models/domain/organization";
|
||||
import { ProfileOrganizationResponse } from "../../../admin-console/models/response/profile-organization.response";
|
||||
import { KeyConnectorUserKeyResponse } from "../../../auth/models/response/key-connector-user-key.response";
|
||||
import { TokenService } from "../../../auth/services/token.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { KeyGenerationService } from "../../../platform/services/key-generation.service";
|
||||
import { OrganizationId, UserId } from "../../../types/guid";
|
||||
import { MasterKey } from "../../../types/key";
|
||||
import { FakeMasterPasswordService } from "../../master-password/services/fake-master-password.service";
|
||||
import { KeyConnectorUserKeyRequest } from "../models/key-connector-user-key.request";
|
||||
|
||||
import {
|
||||
USES_KEY_CONNECTOR,
|
||||
CONVERT_ACCOUNT_TO_KEY_CONNECTOR,
|
||||
KeyConnectorService,
|
||||
} from "./key-connector.service";
|
||||
import { FakeMasterPasswordService } from "./master-password/fake-master-password.service";
|
||||
import { TokenService } from "./token.service";
|
||||
|
||||
describe("KeyConnectorService", () => {
|
||||
let keyConnectorService: KeyConnectorService;
|
||||
@@ -3,8 +3,6 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
Argon2KdfConfig,
|
||||
KdfConfig,
|
||||
@@ -13,28 +11,30 @@ import {
|
||||
KdfType,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { OrganizationUserType } from "../../admin-console/enums";
|
||||
import { Organization } from "../../admin-console/models/domain/organization";
|
||||
import { KeysRequest } from "../../models/request/keys.request";
|
||||
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { OrganizationService } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationUserType } from "../../../admin-console/enums";
|
||||
import { Organization } from "../../../admin-console/models/domain/organization";
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { TokenService } from "../../../auth/abstractions/token.service";
|
||||
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
|
||||
import { KeysRequest } from "../../../models/request/keys.request";
|
||||
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
ActiveUserState,
|
||||
KEY_CONNECTOR_DISK,
|
||||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
} from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { MasterKey } from "../../types/key";
|
||||
} from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey } from "../../../types/key";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../models/request/set-key-connector-key.request";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
import { KeyConnectorUserKeyRequest } from "../models/key-connector-user-key.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../models/set-key-connector-key.request";
|
||||
|
||||
export const USES_KEY_CONNECTOR = new UserKeyDefinition<boolean | null>(
|
||||
KEY_CONNECTOR_DISK,
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { MasterKey, UserKey } from "../../types/key";
|
||||
import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason";
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey, UserKey } from "../../../types/key";
|
||||
|
||||
export abstract class MasterPasswordServiceAbstraction {
|
||||
/**
|
||||
@@ -3,11 +3,11 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { ReplaySubject, Observable } from "rxjs";
|
||||
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey, UserKey } from "../../../types/key";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction";
|
||||
|
||||
export class FakeMasterPasswordService implements InternalMasterPasswordServiceAbstraction {
|
||||
mock = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
} from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey, UserKey } from "../../../types/key";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason";
|
||||
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction";
|
||||
|
||||
/** Memory since master key shouldn't be available on lock */
|
||||
const MASTER_KEY = new UserKeyDefinition<MasterKey>(MASTER_PASSWORD_MEMORY, "masterKey", {
|
||||
@@ -175,7 +175,7 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
"returns $expected when policy is $policy, and user preference is $userPreference",
|
||||
async ({ policy, userPreference, expected }) => {
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
policyService.getAll$.mockReturnValue(
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])),
|
||||
);
|
||||
|
||||
@@ -213,7 +213,7 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
userDecryptionOptionsSubject.next(
|
||||
new UserDecryptionOptions({ hasMasterPassword: false }),
|
||||
);
|
||||
policyService.getAll$.mockReturnValue(
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])),
|
||||
);
|
||||
|
||||
@@ -257,7 +257,7 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
"when policy is %s, and vault timeout is %s, returns %s",
|
||||
async (policy, vaultTimeout, expected) => {
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
policyService.getAll$.mockReturnValue(
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of(policy === null ? [] : ([{ data: { minutes: policy } }] as unknown as Policy[])),
|
||||
);
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
distinctUntilChanged,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
tap,
|
||||
@@ -24,6 +23,7 @@ import { BiometricStateService, KeyService } from "@bitwarden/key-management";
|
||||
import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { getFirstPolicy } from "../../../admin-console/services/policy/default-policy.service";
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { TokenService } from "../../../auth/abstractions/token.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
@@ -266,8 +266,8 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
||||
}
|
||||
|
||||
return this.policyService
|
||||
.getAll$(PolicyType.MaximumVaultTimeout, userId)
|
||||
.pipe(map((policies) => policies[0] ?? null));
|
||||
.policiesByType$(PolicyType.MaximumVaultTimeout, userId)
|
||||
.pipe(getFirstPolicy);
|
||||
}
|
||||
|
||||
private async getAvailableVaultTimeoutActions(userId?: string): Promise<VaultTimeoutAction[]> {
|
||||
|
||||
@@ -12,7 +12,6 @@ import { SearchService } from "../../../abstractions/search.service";
|
||||
import { AccountInfo } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
import { FakeMasterPasswordService } from "../../../auth/services/master-password/fake-master-password.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../../platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
|
||||
@@ -23,6 +22,7 @@ import { StateEventRunnerService } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { CipherService } from "../../../vault/abstractions/cipher.service";
|
||||
import { FolderService } from "../../../vault/abstractions/folder/folder.service.abstraction";
|
||||
import { FakeMasterPasswordService } from "../../master-password/services/fake-master-password.service";
|
||||
import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum";
|
||||
import { VaultTimeout, VaultTimeoutStringType } from "../types/vault-timeout.type";
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { BiometricsService } from "@bitwarden/key-management";
|
||||
import { SearchService } from "../../../abstractions/search.service";
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../../platform/abstractions/messaging.service";
|
||||
@@ -20,6 +19,7 @@ import { StateEventRunnerService } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { CipherService } from "../../../vault/abstractions/cipher.service";
|
||||
import { FolderService } from "../../../vault/abstractions/folder/folder.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../master-password/abstractions/master-password.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "../abstractions/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../abstractions/vault-timeout.service";
|
||||
import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum";
|
||||
|
||||
@@ -73,6 +73,7 @@ export class CipherExport {
|
||||
break;
|
||||
case CipherType.SshKey:
|
||||
view.sshKey = SshKeyExport.toView(req.sshKey);
|
||||
break;
|
||||
}
|
||||
|
||||
if (req.passwordHistory != null) {
|
||||
|
||||
@@ -24,9 +24,9 @@ export class SshKeyExport {
|
||||
}
|
||||
|
||||
static toDomain(req: SshKeyExport, domain = new SshKeyDomain()) {
|
||||
domain.privateKey = req.privateKey != null ? new EncString(req.privateKey) : null;
|
||||
domain.publicKey = req.publicKey != null ? new EncString(req.publicKey) : null;
|
||||
domain.keyFingerprint = req.keyFingerprint != null ? new EncString(req.keyFingerprint) : null;
|
||||
domain.privateKey = new EncString(req.privateKey);
|
||||
domain.publicKey = new EncString(req.publicKey);
|
||||
domain.keyFingerprint = new EncString(req.keyFingerprint);
|
||||
return domain;
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ export abstract class Fido2UserInterfaceSession {
|
||||
*
|
||||
* @param params The parameters to use when asking the user to pick a credential.
|
||||
* @param abortController An abort controller that can be used to cancel/close the session.
|
||||
* @returns The ID of the cipher that contains the credentials the user picked.
|
||||
* @returns The ID of the cipher that contains the credentials the user picked. If not cipher was picked, return cipherId = undefined to to let the authenticator throw the error.
|
||||
*/
|
||||
pickCredential: (
|
||||
params: PickCredentialParams,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export enum EncryptionType {
|
||||
AesCbc256_B64 = 0,
|
||||
AesCbc128_HmacSha256_B64 = 1,
|
||||
// Type 1 was the unused and removed AesCbc128_HmacSha256_B64
|
||||
AesCbc256_HmacSha256_B64 = 2,
|
||||
Rsa2048_OaepSha256_B64 = 3,
|
||||
Rsa2048_OaepSha1_B64 = 4,
|
||||
@@ -17,12 +17,10 @@ export function encryptionTypeToString(encryptionType: EncryptionType): string {
|
||||
}
|
||||
|
||||
/** The expected number of parts to a serialized EncString of the given encryption type.
|
||||
* For example, an EncString of type AesCbc256_B64 will have 2 parts, and an EncString of type
|
||||
* AesCbc128_HmacSha256_B64 will have 3 parts.
|
||||
* For example, an EncString of type AesCbc256_B64 will have 2 parts
|
||||
*
|
||||
* Example of annotated serialized EncStrings:
|
||||
* 0.iv|data
|
||||
* 1.iv|data|mac
|
||||
* 2.iv|data|mac
|
||||
* 3.data
|
||||
* 4.data
|
||||
@@ -33,7 +31,6 @@ export function encryptionTypeToString(encryptionType: EncryptionType): string {
|
||||
*/
|
||||
export const EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE = {
|
||||
[EncryptionType.AesCbc256_B64]: 2,
|
||||
[EncryptionType.AesCbc128_HmacSha256_B64]: 3,
|
||||
[EncryptionType.AesCbc256_HmacSha256_B64]: 3,
|
||||
[EncryptionType.Rsa2048_OaepSha256_B64]: 1,
|
||||
[EncryptionType.Rsa2048_OaepSha1_B64]: 1,
|
||||
|
||||
@@ -5,16 +5,12 @@ export enum ThemeType {
|
||||
System = "system",
|
||||
Light = "light",
|
||||
Dark = "dark",
|
||||
Nord = "nord",
|
||||
SolarizedDark = "solarizedDark",
|
||||
}
|
||||
|
||||
export const ThemeTypes = {
|
||||
System: "system",
|
||||
Light: "light",
|
||||
Dark: "dark",
|
||||
Nord: "nord",
|
||||
SolarizedDark: "solarizedDark",
|
||||
} as const;
|
||||
|
||||
export type Theme = (typeof ThemeTypes)[keyof typeof ThemeTypes];
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import { clearCaches, sequentialize } from "./sequentialize";
|
||||
|
||||
describe("sequentialize decorator", () => {
|
||||
it("should call the function once", async () => {
|
||||
const foo = new Foo();
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(foo.bar(1));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(foo.calls).toBe(1);
|
||||
});
|
||||
|
||||
it("should call the function once for each instance of the object", async () => {
|
||||
const foo = new Foo();
|
||||
const foo2 = new Foo();
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(foo.bar(1));
|
||||
promises.push(foo2.bar(1));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(foo.calls).toBe(1);
|
||||
expect(foo2.calls).toBe(1);
|
||||
});
|
||||
|
||||
it("should call the function once with key function", async () => {
|
||||
const foo = new Foo();
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(foo.baz(1));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(foo.calls).toBe(1);
|
||||
});
|
||||
|
||||
it("should call the function again when already resolved", async () => {
|
||||
const foo = new Foo();
|
||||
await foo.bar(1);
|
||||
expect(foo.calls).toBe(1);
|
||||
await foo.bar(1);
|
||||
expect(foo.calls).toBe(2);
|
||||
});
|
||||
|
||||
it("should call the function again when already resolved with a key function", async () => {
|
||||
const foo = new Foo();
|
||||
await foo.baz(1);
|
||||
expect(foo.calls).toBe(1);
|
||||
await foo.baz(1);
|
||||
expect(foo.calls).toBe(2);
|
||||
});
|
||||
|
||||
it("should call the function for each argument", async () => {
|
||||
const foo = new Foo();
|
||||
await Promise.all([foo.bar(1), foo.bar(1), foo.bar(2), foo.bar(2), foo.bar(3), foo.bar(3)]);
|
||||
expect(foo.calls).toBe(3);
|
||||
});
|
||||
|
||||
it("should call the function for each argument with key function", async () => {
|
||||
const foo = new Foo();
|
||||
await Promise.all([foo.baz(1), foo.baz(1), foo.baz(2), foo.baz(2), foo.baz(3), foo.baz(3)]);
|
||||
expect(foo.calls).toBe(3);
|
||||
});
|
||||
|
||||
it("should return correct result for each call", async () => {
|
||||
const foo = new Foo();
|
||||
const allRes: number[] = [];
|
||||
|
||||
await Promise.all([
|
||||
foo.bar(1).then((res) => allRes.push(res)),
|
||||
foo.bar(1).then((res) => allRes.push(res)),
|
||||
foo.bar(2).then((res) => allRes.push(res)),
|
||||
foo.bar(2).then((res) => allRes.push(res)),
|
||||
foo.bar(3).then((res) => allRes.push(res)),
|
||||
foo.bar(3).then((res) => allRes.push(res)),
|
||||
]);
|
||||
expect(foo.calls).toBe(3);
|
||||
expect(allRes.length).toBe(6);
|
||||
allRes.sort();
|
||||
expect(allRes).toEqual([2, 2, 4, 4, 6, 6]);
|
||||
});
|
||||
|
||||
it("should return correct result for each call with key function", async () => {
|
||||
const foo = new Foo();
|
||||
const allRes: number[] = [];
|
||||
|
||||
await Promise.all([
|
||||
foo.baz(1).then((res) => allRes.push(res)),
|
||||
foo.baz(1).then((res) => allRes.push(res)),
|
||||
foo.baz(2).then((res) => allRes.push(res)),
|
||||
foo.baz(2).then((res) => allRes.push(res)),
|
||||
foo.baz(3).then((res) => allRes.push(res)),
|
||||
foo.baz(3).then((res) => allRes.push(res)),
|
||||
]);
|
||||
expect(foo.calls).toBe(3);
|
||||
expect(allRes.length).toBe(6);
|
||||
allRes.sort();
|
||||
expect(allRes).toEqual([3, 3, 6, 6, 9, 9]);
|
||||
});
|
||||
|
||||
describe("clearCaches", () => {
|
||||
it("should clear all caches", async () => {
|
||||
const foo = new Foo();
|
||||
const promise = Promise.all([foo.bar(1), foo.bar(1)]);
|
||||
clearCaches();
|
||||
await foo.bar(1);
|
||||
await promise;
|
||||
// one call for the first two, one for the third after the cache was cleared
|
||||
expect(foo.calls).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class Foo {
|
||||
calls = 0;
|
||||
|
||||
@sequentialize((args) => "bar" + args[0])
|
||||
bar(a: number): Promise<number> {
|
||||
this.calls++;
|
||||
return new Promise((res) => {
|
||||
setTimeout(() => {
|
||||
res(a * 2);
|
||||
}, Math.random() * 100);
|
||||
});
|
||||
}
|
||||
|
||||
@sequentialize((args) => "baz" + args[0])
|
||||
baz(a: number): Promise<number> {
|
||||
this.calls++;
|
||||
return new Promise((res) => {
|
||||
setTimeout(() => {
|
||||
res(a * 3);
|
||||
}, Math.random() * 100);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
const caches = new Map<any, Map<string, Promise<any>>>();
|
||||
|
||||
const getCache = (obj: any) => {
|
||||
let cache = caches.get(obj);
|
||||
if (cache != null) {
|
||||
return cache;
|
||||
}
|
||||
cache = new Map<string, Promise<any>>();
|
||||
caches.set(obj, cache);
|
||||
return cache;
|
||||
};
|
||||
|
||||
export function clearCaches() {
|
||||
caches.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use as a Decorator on async functions, it will prevent multiple 'active' calls as the same time
|
||||
*
|
||||
* If a promise was returned from a previous call to this function, that hasn't yet resolved it will
|
||||
* be returned, instead of calling the original function again
|
||||
*
|
||||
* Results are not cached, once the promise has returned, the next call will result in a fresh call
|
||||
*
|
||||
* Read more at https://github.com/bitwarden/jslib/pull/7
|
||||
*/
|
||||
export function sequentialize(cacheKey: (args: any[]) => string) {
|
||||
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
|
||||
const originalMethod: () => Promise<any> = descriptor.value;
|
||||
|
||||
return {
|
||||
value: function (...args: any[]) {
|
||||
const cache = getCache(this);
|
||||
const argsCacheKey = cacheKey(args);
|
||||
let response = cache.get(argsCacheKey);
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const onFinally = () => {
|
||||
cache.delete(argsCacheKey);
|
||||
if (cache.size === 0) {
|
||||
caches.delete(this);
|
||||
}
|
||||
};
|
||||
response = originalMethod
|
||||
.apply(this, args)
|
||||
.then((val: any) => {
|
||||
onFinally();
|
||||
return val;
|
||||
})
|
||||
.catch((err: any) => {
|
||||
onFinally();
|
||||
throw err;
|
||||
});
|
||||
|
||||
cache.set(argsCacheKey, response);
|
||||
return response;
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { sequentialize } from "./sequentialize";
|
||||
import { throttle } from "./throttle";
|
||||
|
||||
describe("throttle decorator", () => {
|
||||
@@ -51,17 +50,6 @@ describe("throttle decorator", () => {
|
||||
expect(foo.calls).toBe(10);
|
||||
expect(foo2.calls).toBe(10);
|
||||
});
|
||||
|
||||
it("should work together with sequentialize", async () => {
|
||||
const foo = new Foo();
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(foo.qux(Math.floor(i / 2) * 2));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(foo.calls).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
class Foo {
|
||||
@@ -94,7 +82,6 @@ class Foo {
|
||||
});
|
||||
}
|
||||
|
||||
@sequentialize((args) => "qux" + args[0])
|
||||
@throttle(1, () => "qux")
|
||||
qux(a: number) {
|
||||
this.calls++;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ConditionalExcept, ConditionalKeys, Constructor } from "type-fest";
|
||||
|
||||
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -15,6 +13,19 @@ export type DecryptedObject<
|
||||
TDecryptedKeys extends EncStringKeys<TEncryptedObject>,
|
||||
> = Record<TDecryptedKeys, string> & Omit<TEncryptedObject, TDecryptedKeys>;
|
||||
|
||||
// extracts shared keys from the domain and view types
|
||||
type EncryptableKeys<D extends Domain, V extends View> = (keyof D &
|
||||
ConditionalKeys<D, EncString | null>) &
|
||||
(keyof V & ConditionalKeys<V, string | null>);
|
||||
|
||||
type DomainEncryptableKeys<D extends Domain> = {
|
||||
[key in ConditionalKeys<D, EncString | null>]: EncString | null;
|
||||
};
|
||||
|
||||
type ViewEncryptableKeys<V extends View> = {
|
||||
[key in ConditionalKeys<V, string | null>]: string | null;
|
||||
};
|
||||
|
||||
// https://contributing.bitwarden.com/architecture/clients/data-model#domain
|
||||
export default class Domain {
|
||||
protected buildDomainModel<D extends Domain>(
|
||||
@@ -37,6 +48,7 @@ export default class Domain {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected buildDataModel<D extends Domain>(
|
||||
domain: D,
|
||||
dataObj: any,
|
||||
@@ -58,31 +70,24 @@ export default class Domain {
|
||||
}
|
||||
}
|
||||
|
||||
protected async decryptObj<T extends View>(
|
||||
viewModel: T,
|
||||
map: any,
|
||||
orgId: string,
|
||||
key: SymmetricCryptoKey = null,
|
||||
protected async decryptObj<D extends Domain, V extends View>(
|
||||
domain: DomainEncryptableKeys<D>,
|
||||
viewModel: ViewEncryptableKeys<V>,
|
||||
props: EncryptableKeys<D, V>[],
|
||||
orgId: string | null,
|
||||
key: SymmetricCryptoKey | null = null,
|
||||
objectContext: string = "No Domain Context",
|
||||
): Promise<T> {
|
||||
const self: any = this;
|
||||
|
||||
for (const prop in map) {
|
||||
// eslint-disable-next-line
|
||||
if (!map.hasOwnProperty(prop)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mapProp = map[prop] || prop;
|
||||
if (self[mapProp]) {
|
||||
(viewModel as any)[prop] = await self[mapProp].decrypt(
|
||||
): Promise<V> {
|
||||
for (const prop of props) {
|
||||
viewModel[prop] =
|
||||
(await domain[prop]?.decrypt(
|
||||
orgId,
|
||||
key,
|
||||
`Property: ${prop}; ObjectContext: ${objectContext}`,
|
||||
);
|
||||
}
|
||||
`Property: ${prop as string}; ObjectContext: ${objectContext}`,
|
||||
)) ?? null;
|
||||
}
|
||||
return viewModel;
|
||||
|
||||
return viewModel as V;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,7 +116,7 @@ export default class Domain {
|
||||
const decryptedObjects = [];
|
||||
|
||||
for (const prop of encryptedProperties) {
|
||||
const value = (this as any)[prop] as EncString;
|
||||
const value = this[prop] as EncString;
|
||||
const decrypted = await this.decryptProperty(
|
||||
prop,
|
||||
value,
|
||||
@@ -138,11 +143,9 @@ export default class Domain {
|
||||
encryptService: EncryptService,
|
||||
decryptTrace: string,
|
||||
) {
|
||||
let decrypted: string = null;
|
||||
let decrypted: string | null = null;
|
||||
if (value) {
|
||||
decrypted = await value.decryptWithKey(key, encryptService, decryptTrace);
|
||||
} else {
|
||||
decrypted = null;
|
||||
}
|
||||
return {
|
||||
[propertyKey]: decrypted,
|
||||
|
||||
@@ -5,28 +5,28 @@ import { EncArrayBuffer } from "./enc-array-buffer";
|
||||
|
||||
describe("encArrayBuffer", () => {
|
||||
describe("parses the buffer", () => {
|
||||
test.each([
|
||||
[EncryptionType.AesCbc128_HmacSha256_B64, "AesCbc128_HmacSha256_B64"],
|
||||
[EncryptionType.AesCbc256_HmacSha256_B64, "AesCbc256_HmacSha256_B64"],
|
||||
])("with %c%s", (encType: EncryptionType) => {
|
||||
const iv = makeStaticByteArray(16, 10);
|
||||
const mac = makeStaticByteArray(32, 20);
|
||||
// We use the minimum data length of 1 to test the boundary of valid lengths
|
||||
const data = makeStaticByteArray(1, 100);
|
||||
test.each([[EncryptionType.AesCbc256_HmacSha256_B64, "AesCbc256_HmacSha256_B64"]])(
|
||||
"with %c%s",
|
||||
(encType: EncryptionType) => {
|
||||
const iv = makeStaticByteArray(16, 10);
|
||||
const mac = makeStaticByteArray(32, 20);
|
||||
// We use the minimum data length of 1 to test the boundary of valid lengths
|
||||
const data = makeStaticByteArray(1, 100);
|
||||
|
||||
const array = new Uint8Array(1 + iv.byteLength + mac.byteLength + data.byteLength);
|
||||
array.set([encType]);
|
||||
array.set(iv, 1);
|
||||
array.set(mac, 1 + iv.byteLength);
|
||||
array.set(data, 1 + iv.byteLength + mac.byteLength);
|
||||
const array = new Uint8Array(1 + iv.byteLength + mac.byteLength + data.byteLength);
|
||||
array.set([encType]);
|
||||
array.set(iv, 1);
|
||||
array.set(mac, 1 + iv.byteLength);
|
||||
array.set(data, 1 + iv.byteLength + mac.byteLength);
|
||||
|
||||
const actual = new EncArrayBuffer(array);
|
||||
const actual = new EncArrayBuffer(array);
|
||||
|
||||
expect(actual.encryptionType).toEqual(encType);
|
||||
expect(actual.ivBytes).toEqualBuffer(iv);
|
||||
expect(actual.macBytes).toEqualBuffer(mac);
|
||||
expect(actual.dataBytes).toEqualBuffer(data);
|
||||
});
|
||||
expect(actual.encryptionType).toEqual(encType);
|
||||
expect(actual.ivBytes).toEqualBuffer(iv);
|
||||
expect(actual.macBytes).toEqualBuffer(mac);
|
||||
expect(actual.dataBytes).toEqualBuffer(data);
|
||||
},
|
||||
);
|
||||
|
||||
it("with AesCbc256_B64", () => {
|
||||
const encType = EncryptionType.AesCbc256_B64;
|
||||
@@ -50,7 +50,6 @@ describe("encArrayBuffer", () => {
|
||||
|
||||
describe("throws if the buffer has an invalid length", () => {
|
||||
test.each([
|
||||
[EncryptionType.AesCbc128_HmacSha256_B64, 50, "AesCbc128_HmacSha256_B64"],
|
||||
[EncryptionType.AesCbc256_HmacSha256_B64, 50, "AesCbc256_HmacSha256_B64"],
|
||||
[EncryptionType.AesCbc256_B64, 18, "AesCbc256_B64"],
|
||||
])("with %c%c%s", (encType: EncryptionType, minLength: number) => {
|
||||
|
||||
@@ -20,7 +20,6 @@ export class EncArrayBuffer implements Encrypted {
|
||||
const encType = encBytes[0];
|
||||
|
||||
switch (encType) {
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case EncryptionType.AesCbc256_HmacSha256_B64: {
|
||||
const minimumLength = ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH + MIN_DATA_LENGTH;
|
||||
if (encBytes.length < minimumLength) {
|
||||
|
||||
@@ -60,9 +60,7 @@ describe("EncString", () => {
|
||||
|
||||
const cases = [
|
||||
"aXY=|Y3Q=", // AesCbc256_B64 w/out header
|
||||
"aXY=|Y3Q=|cnNhQ3Q=", // AesCbc128_HmacSha256_B64 w/out header
|
||||
"0.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==", // AesCbc256_B64 with header
|
||||
"1.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==", // AesCbc128_HmacSha256_B64
|
||||
"2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==", // AesCbc256_HmacSha256_B64
|
||||
"3.QmFzZTY0UGFydA==", // Rsa2048_OaepSha256_B64
|
||||
"4.QmFzZTY0UGFydA==", // Rsa2048_OaepSha1_B64
|
||||
|
||||
@@ -89,7 +89,6 @@ export class EncString implements Encrypted {
|
||||
}
|
||||
|
||||
switch (encType) {
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case EncryptionType.AesCbc256_HmacSha256_B64:
|
||||
this.iv = encPieces[0];
|
||||
this.data = encPieces[1];
|
||||
@@ -132,10 +131,7 @@ export class EncString implements Encrypted {
|
||||
}
|
||||
} else {
|
||||
encPieces = encryptedString.split("|");
|
||||
encType =
|
||||
encPieces.length === 3
|
||||
? EncryptionType.AesCbc128_HmacSha256_B64
|
||||
: EncryptionType.AesCbc256_B64;
|
||||
encType = EncryptionType.AesCbc256_B64;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -160,7 +156,7 @@ export class EncString implements Encrypted {
|
||||
|
||||
async decrypt(
|
||||
orgId: string | null,
|
||||
key: SymmetricCryptoKey = null,
|
||||
key: SymmetricCryptoKey | null = null,
|
||||
context?: string,
|
||||
): Promise<string> {
|
||||
if (this.decryptedValue != null) {
|
||||
|
||||
@@ -27,21 +27,6 @@ describe("SymmetricCryptoKey", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("AesCbc128_HmacSha256_B64", () => {
|
||||
const key = makeStaticByteArray(32);
|
||||
const cryptoKey = new SymmetricCryptoKey(key, EncryptionType.AesCbc128_HmacSha256_B64);
|
||||
|
||||
expect(cryptoKey).toEqual({
|
||||
encKey: key.slice(0, 16),
|
||||
encKeyB64: "AAECAwQFBgcICQoLDA0ODw==",
|
||||
encType: 1,
|
||||
key: key,
|
||||
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
|
||||
macKey: key.slice(16, 32),
|
||||
macKeyB64: "EBESExQVFhcYGRobHB0eHw==",
|
||||
});
|
||||
});
|
||||
|
||||
it("AesCbc256_HmacSha256_B64", () => {
|
||||
const key = makeStaticByteArray(64);
|
||||
const cryptoKey = new SymmetricCryptoKey(key);
|
||||
|
||||
@@ -38,9 +38,6 @@ export class SymmetricCryptoKey {
|
||||
if (encType === EncryptionType.AesCbc256_B64 && key.byteLength === 32) {
|
||||
this.encKey = key;
|
||||
this.macKey = null;
|
||||
} else if (encType === EncryptionType.AesCbc128_HmacSha256_B64 && key.byteLength === 32) {
|
||||
this.encKey = key.slice(0, 16);
|
||||
this.macKey = key.slice(16, 32);
|
||||
} else if (encType === EncryptionType.AesCbc256_HmacSha256_B64 && key.byteLength === 64) {
|
||||
this.encKey = key.slice(0, 32);
|
||||
this.macKey = key.slice(32, 64);
|
||||
|
||||
@@ -225,9 +225,10 @@ describe("NotificationsService", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Unlocked },
|
||||
{ initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Locked },
|
||||
{ initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Locked },
|
||||
// Temporarily rolling back notifications being connected while locked
|
||||
// { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Unlocked },
|
||||
// { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Locked },
|
||||
// { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Locked },
|
||||
{ initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Unlocked },
|
||||
])(
|
||||
"does not re-connect when the user transitions from $initialStatus to $updatedStatus",
|
||||
@@ -252,7 +253,11 @@ describe("NotificationsService", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each([AuthenticationStatus.Locked, AuthenticationStatus.Unlocked])(
|
||||
it.each([
|
||||
// Temporarily disabling notifications connecting while in a locked state
|
||||
// AuthenticationStatus.Locked,
|
||||
AuthenticationStatus.Unlocked,
|
||||
])(
|
||||
"connects when a user transitions from logged out to %s",
|
||||
async (newStatus: AuthenticationStatus) => {
|
||||
emitActiveUser(mockUser1);
|
||||
|
||||
@@ -123,13 +123,13 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract
|
||||
);
|
||||
}
|
||||
|
||||
// This method name is a lie currently as we also have an access token
|
||||
// when locked, this is eventually where we want to be but it increases load
|
||||
// on signalR so we are rolling back until we can move the load of browser to
|
||||
// web push.
|
||||
private hasAccessToken$(userId: UserId) {
|
||||
return this.authService.authStatusFor$(userId).pipe(
|
||||
map(
|
||||
(authStatus) =>
|
||||
authStatus === AuthenticationStatus.Locked ||
|
||||
authStatus === AuthenticationStatus.Unlocked,
|
||||
),
|
||||
map((authStatus) => authStatus === AuthenticationStatus.Unlocked),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { Subscription } from "rxjs";
|
||||
import { Observable, Subject, Subscription } from "rxjs";
|
||||
|
||||
import { NotificationResponse } from "@bitwarden/common/models/response/notification.response";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { NotificationsService } from "../notifications.service";
|
||||
|
||||
export class NoopNotificationsService implements NotificationsService {
|
||||
notifications$: Observable<readonly [NotificationResponse, UserId]> = new Subject();
|
||||
|
||||
constructor(private logService: LogService) {}
|
||||
|
||||
startListening(): Subscription {
|
||||
|
||||
@@ -23,6 +23,11 @@ export type ReceiveMessage = { type: "ReceiveMessage"; message: NotificationResp
|
||||
|
||||
export type SignalRNotification = Heartbeat | ReceiveMessage;
|
||||
|
||||
export type TimeoutManager = {
|
||||
setTimeout: (handler: TimerHandler, timeout: number) => number;
|
||||
clearTimeout: (timeoutId: number) => void;
|
||||
};
|
||||
|
||||
class SignalRLogger implements ILogger {
|
||||
constructor(private readonly logService: LogService) {}
|
||||
|
||||
@@ -51,11 +56,14 @@ export class SignalRConnectionService {
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly logService: LogService,
|
||||
private readonly hubConnectionBuilderFactory: () => HubConnectionBuilder = () =>
|
||||
new HubConnectionBuilder(),
|
||||
private readonly timeoutManager: TimeoutManager = globalThis,
|
||||
) {}
|
||||
|
||||
connect$(userId: UserId, notificationsUrl: string) {
|
||||
return new Observable<SignalRNotification>((subsciber) => {
|
||||
const connection = new HubConnectionBuilder()
|
||||
const connection = this.hubConnectionBuilderFactory()
|
||||
.withUrl(notificationsUrl + "/hub", {
|
||||
accessTokenFactory: () => this.apiService.getActiveBearerToken(),
|
||||
skipNegotiation: true,
|
||||
@@ -76,48 +84,60 @@ export class SignalRConnectionService {
|
||||
let reconnectSubscription: Subscription | null = null;
|
||||
|
||||
// Create schedule reconnect function
|
||||
const scheduleReconnect = (): Subscription => {
|
||||
const scheduleReconnect = () => {
|
||||
if (
|
||||
connection == null ||
|
||||
connection.state !== HubConnectionState.Disconnected ||
|
||||
(reconnectSubscription != null && !reconnectSubscription.closed)
|
||||
) {
|
||||
return Subscription.EMPTY;
|
||||
// Skip scheduling a new reconnect, either the connection isn't disconnected
|
||||
// or an active reconnect is already scheduled.
|
||||
return;
|
||||
}
|
||||
|
||||
const randomTime = this.random();
|
||||
const timeoutHandler = setTimeout(() => {
|
||||
// If we've somehow gotten here while the subscriber is closed,
|
||||
// we do not want to reconnect. So leave.
|
||||
if (subsciber.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const randomTime = this.randomReconnectTime();
|
||||
const timeoutHandler = this.timeoutManager.setTimeout(() => {
|
||||
connection
|
||||
.start()
|
||||
.then(() => (reconnectSubscription = null))
|
||||
.then(() => {
|
||||
reconnectSubscription = null;
|
||||
})
|
||||
.catch(() => {
|
||||
reconnectSubscription = scheduleReconnect();
|
||||
scheduleReconnect();
|
||||
});
|
||||
}, randomTime);
|
||||
|
||||
return new Subscription(() => clearTimeout(timeoutHandler));
|
||||
reconnectSubscription = new Subscription(() =>
|
||||
this.timeoutManager.clearTimeout(timeoutHandler),
|
||||
);
|
||||
};
|
||||
|
||||
connection.onclose((error) => {
|
||||
reconnectSubscription = scheduleReconnect();
|
||||
scheduleReconnect();
|
||||
});
|
||||
|
||||
// Start connection
|
||||
connection.start().catch(() => {
|
||||
reconnectSubscription = scheduleReconnect();
|
||||
scheduleReconnect();
|
||||
});
|
||||
|
||||
return () => {
|
||||
// Cancel any possible scheduled reconnects
|
||||
reconnectSubscription?.unsubscribe();
|
||||
connection?.stop().catch((error) => {
|
||||
this.logService.error("Error while stopping SignalR connection", error);
|
||||
// TODO: Does calling stop call `onclose`?
|
||||
reconnectSubscription?.unsubscribe();
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private random() {
|
||||
private randomReconnectTime() {
|
||||
return (
|
||||
Math.floor(Math.random() * (MAX_RECONNECT_TIME - MIN_RECONNECT_TIME + 1)) + MIN_RECONNECT_TIME
|
||||
);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Subscription } from "rxjs";
|
||||
import { Observable, Subscription } from "rxjs";
|
||||
|
||||
import { NotificationResponse } from "@bitwarden/common/models/response/notification.response";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
/**
|
||||
* A service offering abilities to interact with push notifications from the server.
|
||||
*/
|
||||
export abstract class NotificationsService {
|
||||
abstract notifications$: Observable<readonly [NotificationResponse, UserId]>;
|
||||
/**
|
||||
* Starts automatic listening and processing of notifications, should only be called once per application,
|
||||
* or you will risk notifications being processed multiple times.
|
||||
|
||||
@@ -251,7 +251,8 @@ export class Fido2ClientService<ParentWindowReference>
|
||||
clientDataJSON: Fido2Utils.bufferToString(clientDataJSONBytes),
|
||||
publicKey: Fido2Utils.bufferToString(makeCredentialResult.publicKey),
|
||||
publicKeyAlgorithm: makeCredentialResult.publicKeyAlgorithm,
|
||||
transports: params.rp.id === "google.com" ? ["internal", "usb"] : ["internal"],
|
||||
transports:
|
||||
params.rp.id === "google.com" ? ["internal", "usb", "hybrid"] : ["internal", "hybrid"],
|
||||
extensions: { credProps },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -131,6 +131,10 @@ export const THEMING_DISK = new StateDefinition("theming", "disk", { web: "disk-
|
||||
export const TRANSLATION_DISK = new StateDefinition("translation", "disk", { web: "disk-local" });
|
||||
export const ANIMATION_DISK = new StateDefinition("animation", "disk");
|
||||
export const TASK_SCHEDULER_DISK = new StateDefinition("taskScheduler", "disk");
|
||||
export const EXTENSION_INITIAL_INSTALL_DISK = new StateDefinition(
|
||||
"extensionInitialInstall",
|
||||
"disk",
|
||||
);
|
||||
|
||||
// Design System
|
||||
|
||||
@@ -201,3 +205,4 @@ export const NEW_DEVICE_VERIFICATION_NOTICE = new StateDefinition(
|
||||
export const VAULT_APPEARANCE = new StateDefinition("vaultAppearance", "disk");
|
||||
export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk");
|
||||
export const AT_RISK_PASSWORDS_PAGE_DISK = new StateDefinition("atRiskPasswordsPage", "disk");
|
||||
export const NOTIFICATION_DISK = new StateDefinition("notifications", "disk");
|
||||
|
||||
@@ -27,13 +27,13 @@ import { PolicyResponse } from "../../admin-console/models/response/policy.respo
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||
import { AvatarService } from "../../auth/abstractions/avatar.service";
|
||||
import { KeyConnectorService } from "../../auth/abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "../../auth/abstractions/token.service";
|
||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
|
||||
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
|
||||
import { BillingAccountProfileStateService } from "../../billing/abstractions";
|
||||
import { KeyConnectorService } from "../../key-management/key-connector/abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { DomainsResponse } from "../../models/response/domains.response";
|
||||
import { ProfileResponse } from "../../models/response/profile.response";
|
||||
import { SendData } from "../../tools/send/models/data/send.data";
|
||||
@@ -51,7 +51,6 @@ import { FolderResponse } from "../../vault/models/response/folder.response";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { MessageSender } from "../messaging";
|
||||
import { sequentialize } from "../misc/sequentialize";
|
||||
import { StateProvider } from "../state";
|
||||
|
||||
import { CoreSyncService } from "./core-sync.service";
|
||||
@@ -103,7 +102,6 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
);
|
||||
}
|
||||
|
||||
@sequentialize(() => "fullSync")
|
||||
override async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)));
|
||||
this.syncStarted();
|
||||
|
||||
@@ -1,43 +1,33 @@
|
||||
import { Observable, combineLatest, map } from "rxjs";
|
||||
import { Observable, map } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "../../enums/feature-flag.enum";
|
||||
import { ConfigService } from "../abstractions/config/config.service";
|
||||
import { ThemeType } from "../enums";
|
||||
import { Theme, ThemeTypes } from "../enums";
|
||||
import { GlobalStateProvider, KeyDefinition, THEMING_DISK } from "../state";
|
||||
|
||||
export abstract class ThemeStateService {
|
||||
/**
|
||||
* The users selected theme.
|
||||
*/
|
||||
abstract selectedTheme$: Observable<ThemeType>;
|
||||
abstract selectedTheme$: Observable<Theme>;
|
||||
|
||||
/**
|
||||
* A method for updating the current users configured theme.
|
||||
* @param theme The chosen user theme.
|
||||
*/
|
||||
abstract setSelectedTheme(theme: ThemeType): Promise<void>;
|
||||
abstract setSelectedTheme(theme: Theme): Promise<void>;
|
||||
}
|
||||
|
||||
export const THEME_SELECTION = new KeyDefinition<ThemeType>(THEMING_DISK, "selection", {
|
||||
export const THEME_SELECTION = new KeyDefinition<Theme>(THEMING_DISK, "selection", {
|
||||
deserializer: (s) => s,
|
||||
});
|
||||
|
||||
export class DefaultThemeStateService implements ThemeStateService {
|
||||
private readonly selectedThemeState = this.globalStateProvider.get(THEME_SELECTION);
|
||||
|
||||
selectedTheme$ = combineLatest([
|
||||
this.selectedThemeState.state$,
|
||||
this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh),
|
||||
]).pipe(
|
||||
map(([theme, isExtensionRefresh]) => {
|
||||
// The extension refresh should not allow for Nord or SolarizedDark
|
||||
// Default the user to their system theme
|
||||
if (
|
||||
isExtensionRefresh &&
|
||||
theme != null &&
|
||||
[ThemeType.Nord, ThemeType.SolarizedDark].includes(theme)
|
||||
) {
|
||||
return ThemeType.System;
|
||||
selectedTheme$ = this.selectedThemeState.state$.pipe(
|
||||
map((theme) => {
|
||||
// We used to support additional themes. Since these are no longer supported we return null to default to the system theme.
|
||||
if (theme != null && !Object.values(ThemeTypes).includes(theme)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return theme;
|
||||
@@ -47,11 +37,10 @@ export class DefaultThemeStateService implements ThemeStateService {
|
||||
|
||||
constructor(
|
||||
private globalStateProvider: GlobalStateProvider,
|
||||
private configService: ConfigService,
|
||||
private defaultTheme: ThemeType = ThemeType.System,
|
||||
private defaultTheme: Theme = ThemeTypes.System,
|
||||
) {}
|
||||
|
||||
async setSelectedTheme(theme: ThemeType): Promise<void> {
|
||||
async setSelectedTheme(theme: Theme): Promise<void> {
|
||||
await this.selectedThemeState.update(() => theme, {
|
||||
shouldUpdate: (currentTheme) => currentTheme !== theme,
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { LogoutReason } from "@bitwarden/auth/common";
|
||||
|
||||
import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service";
|
||||
import { OrganizationConnectionType } from "../admin-console/enums";
|
||||
import { CollectionBulkDeleteRequest } from "../admin-console/models/request/collection-bulk-delete.request";
|
||||
import { OrganizationSponsorshipCreateRequest } from "../admin-console/models/request/organization/organization-sponsorship-create.request";
|
||||
import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/request/organization/organization-sponsorship-redeem.request";
|
||||
import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request";
|
||||
@@ -43,7 +44,6 @@ import {
|
||||
} from "../admin-console/models/response/provider/provider-user.response";
|
||||
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
|
||||
import { TokenService } from "../auth/abstractions/token.service";
|
||||
import { AuthRequest } from "../auth/models/request/auth.request";
|
||||
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
|
||||
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
|
||||
import { EmailTokenRequest } from "../auth/models/request/email-token.request";
|
||||
@@ -54,19 +54,12 @@ import { SsoTokenRequest } from "../auth/models/request/identity-token/sso-token
|
||||
import { TokenTwoFactorRequest } from "../auth/models/request/identity-token/token-two-factor.request";
|
||||
import { UserApiTokenRequest } from "../auth/models/request/identity-token/user-api-token.request";
|
||||
import { WebAuthnLoginTokenRequest } from "../auth/models/request/identity-token/webauthn-login-token.request";
|
||||
import { KeyConnectorUserKeyRequest } from "../auth/models/request/key-connector-user-key.request";
|
||||
import { PasswordHintRequest } from "../auth/models/request/password-hint.request";
|
||||
import { PasswordRequest } from "../auth/models/request/password.request";
|
||||
import { PasswordlessAuthRequest } from "../auth/models/request/passwordless-auth.request";
|
||||
import { SecretVerificationRequest } from "../auth/models/request/secret-verification.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../auth/models/request/set-key-connector-key.request";
|
||||
import { SetPasswordRequest } from "../auth/models/request/set-password.request";
|
||||
import { TwoFactorEmailRequest } from "../auth/models/request/two-factor-email.request";
|
||||
import { TwoFactorProviderRequest } from "../auth/models/request/two-factor-provider.request";
|
||||
import { TwoFactorRecoveryRequest } from "../auth/models/request/two-factor-recovery.request";
|
||||
import { UpdateProfileRequest } from "../auth/models/request/update-profile.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "../auth/models/request/update-tde-offboarding-password.request";
|
||||
import { UpdateTempPasswordRequest } from "../auth/models/request/update-temp-password.request";
|
||||
import { UpdateTwoFactorAuthenticatorRequest } from "../auth/models/request/update-two-factor-authenticator.request";
|
||||
import { UpdateTwoFactorDuoRequest } from "../auth/models/request/update-two-factor-duo.request";
|
||||
import { UpdateTwoFactorEmailRequest } from "../auth/models/request/update-two-factor-email.request";
|
||||
@@ -104,9 +97,10 @@ import { PlanResponse } from "../billing/models/response/plan.response";
|
||||
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
|
||||
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
|
||||
import { DeviceType } from "../enums";
|
||||
import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request";
|
||||
import { VaultTimeoutSettingsService } from "../key-management/vault-timeout";
|
||||
import { VaultTimeoutAction } from "../key-management/vault-timeout/enums/vault-timeout-action.enum";
|
||||
import { CollectionBulkDeleteRequest } from "../models/request/collection-bulk-delete.request";
|
||||
import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
|
||||
import { EventRequest } from "../models/request/event.request";
|
||||
import { KdfRequest } from "../models/request/kdf.request";
|
||||
@@ -279,22 +273,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
}
|
||||
|
||||
// TODO: PM-3519: Create and move to AuthRequest Api service
|
||||
// TODO: PM-9724: Remove legacy auth request methods when we remove legacy LoginViaAuthRequestV1Components
|
||||
async postAuthRequest(request: AuthRequest): Promise<AuthRequestResponse> {
|
||||
const r = await this.send("POST", "/auth-requests/", request, false, true);
|
||||
return new AuthRequestResponse(r);
|
||||
}
|
||||
async postAdminAuthRequest(request: AuthRequest): Promise<AuthRequestResponse> {
|
||||
const r = await this.send("POST", "/auth-requests/admin-request", request, true, true);
|
||||
return new AuthRequestResponse(r);
|
||||
}
|
||||
|
||||
async getAuthResponse(id: string, accessCode: string): Promise<AuthRequestResponse> {
|
||||
const path = `/auth-requests/${id}/response?code=${accessCode}`;
|
||||
const r = await this.send("GET", path, null, false, true);
|
||||
return new AuthRequestResponse(r);
|
||||
}
|
||||
|
||||
async getAuthRequest(id: string): Promise<AuthRequestResponse> {
|
||||
const path = `/auth-requests/${id}`;
|
||||
const r = await this.send("GET", path, null, true, true);
|
||||
@@ -374,14 +352,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return this.send("POST", "/accounts/email", request, true, false);
|
||||
}
|
||||
|
||||
postPassword(request: PasswordRequest): Promise<any> {
|
||||
return this.send("POST", "/accounts/password", request, true, false);
|
||||
}
|
||||
|
||||
setPassword(request: SetPasswordRequest): Promise<any> {
|
||||
return this.send("POST", "/accounts/set-password", request, true, false);
|
||||
}
|
||||
|
||||
postSetKeyConnectorKey(request: SetKeyConnectorKeyRequest): Promise<any> {
|
||||
return this.send("POST", "/accounts/set-key-connector-key", request, true, false);
|
||||
}
|
||||
@@ -479,14 +449,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new ApiKeyResponse(r);
|
||||
}
|
||||
|
||||
putUpdateTempPassword(request: UpdateTempPasswordRequest): Promise<any> {
|
||||
return this.send("PUT", "/accounts/update-temp-password", request, true, false);
|
||||
}
|
||||
|
||||
putUpdateTdeOffboardingPassword(request: UpdateTdeOffboardingPasswordRequest): Promise<void> {
|
||||
return this.send("PUT", "/accounts/update-tde-offboarding-password", request, true, false);
|
||||
}
|
||||
|
||||
postConvertToKeyConnector(): Promise<void> {
|
||||
return this.send("POST", "/accounts/convert-to-key-connector", null, true, false);
|
||||
}
|
||||
@@ -1101,10 +1063,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new TwoFactorProviderResponse(r);
|
||||
}
|
||||
|
||||
postTwoFactorRecover(request: TwoFactorRecoveryRequest): Promise<any> {
|
||||
return this.send("POST", "/two-factor/recover", request, false, false);
|
||||
}
|
||||
|
||||
postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise<any> {
|
||||
return this.send("POST", "/two-factor/send-email", request, true, false);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages
|
||||
import { FakeStorageService } from "../../spec/fake-storage.service";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Needed client type enum
|
||||
import { ClientType } from "../enums";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { VendorId } from "../extension";
|
||||
|
||||
import { IntegrationContext } from "./integration-context";
|
||||
import { IntegrationId } from "./integration-id";
|
||||
@@ -8,7 +9,7 @@ import { IntegrationMetadata } from "./integration-metadata";
|
||||
|
||||
const EXAMPLE_META = Object.freeze({
|
||||
// arbitrary
|
||||
id: "simplelogin" as IntegrationId,
|
||||
id: "simplelogin" as IntegrationId & VendorId,
|
||||
name: "Example",
|
||||
// arbitrary
|
||||
extends: ["forwarder"],
|
||||
@@ -34,7 +35,7 @@ describe("IntegrationContext", () => {
|
||||
|
||||
it("throws when the baseurl isn't defined in metadata", () => {
|
||||
const noBaseUrl: IntegrationMetadata = {
|
||||
id: "simplelogin" as IntegrationId, // arbitrary
|
||||
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
|
||||
name: "Example",
|
||||
extends: ["forwarder"], // arbitrary
|
||||
selfHost: "maybe",
|
||||
@@ -56,7 +57,7 @@ describe("IntegrationContext", () => {
|
||||
|
||||
it("ignores settings when selfhost is 'never'", () => {
|
||||
const selfHostNever: IntegrationMetadata = {
|
||||
id: "simplelogin" as IntegrationId, // arbitrary
|
||||
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
|
||||
name: "Example",
|
||||
extends: ["forwarder"], // arbitrary
|
||||
baseUrl: "example.com",
|
||||
@@ -71,7 +72,7 @@ describe("IntegrationContext", () => {
|
||||
|
||||
it("always reads the settings when selfhost is 'always'", () => {
|
||||
const selfHostAlways: IntegrationMetadata = {
|
||||
id: "simplelogin" as IntegrationId, // arbitrary
|
||||
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
|
||||
name: "Example",
|
||||
extends: ["forwarder"], // arbitrary
|
||||
baseUrl: "example.com",
|
||||
@@ -86,7 +87,7 @@ describe("IntegrationContext", () => {
|
||||
|
||||
it("fails when the settings are empty and selfhost is 'always'", () => {
|
||||
const selfHostAlways: IntegrationMetadata = {
|
||||
id: "simplelogin" as IntegrationId, // arbitrary
|
||||
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
|
||||
name: "Example",
|
||||
extends: ["forwarder"], // arbitrary
|
||||
baseUrl: "example.com",
|
||||
@@ -101,7 +102,7 @@ describe("IntegrationContext", () => {
|
||||
|
||||
it("reads from the metadata by default when selfhost is 'maybe'", () => {
|
||||
const selfHostMaybe: IntegrationMetadata = {
|
||||
id: "simplelogin" as IntegrationId, // arbitrary
|
||||
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
|
||||
name: "Example",
|
||||
extends: ["forwarder"], // arbitrary
|
||||
baseUrl: "example.com",
|
||||
@@ -117,7 +118,7 @@ describe("IntegrationContext", () => {
|
||||
|
||||
it("overrides the metadata when selfhost is 'maybe'", () => {
|
||||
const selfHostMaybe: IntegrationMetadata = {
|
||||
id: "simplelogin" as IntegrationId, // arbitrary
|
||||
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
|
||||
name: "Example",
|
||||
extends: ["forwarder"], // arbitrary
|
||||
baseUrl: "example.com",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { VendorId } from "../extension";
|
||||
|
||||
import { ExtensionPointId } from "./extension-point-id";
|
||||
import { IntegrationId } from "./integration-id";
|
||||
|
||||
/** The capabilities and descriptive content for an integration */
|
||||
export type IntegrationMetadata = {
|
||||
/** Uniquely identifies the integrator. */
|
||||
id: IntegrationId;
|
||||
id: IntegrationId & VendorId;
|
||||
|
||||
/** Brand name of the integrator. */
|
||||
name: string;
|
||||
|
||||
@@ -12,7 +12,11 @@ export class DisabledSemanticLogger implements SemanticLogger {
|
||||
|
||||
error<T>(_content: Jsonify<T>, _message?: string): void {}
|
||||
|
||||
panic<T>(_content: Jsonify<T>, message?: string): never {
|
||||
throw new Error(message);
|
||||
panic<T>(content: Jsonify<T>, message?: string): never {
|
||||
if (typeof content === "string" && !message) {
|
||||
throw new Error(content);
|
||||
} else {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface SemanticLogger {
|
||||
*/
|
||||
debug(message: string): void;
|
||||
|
||||
// FIXME: replace Jsonify<T> parameter with structural logging schema type
|
||||
/** Logs the content at debug priority.
|
||||
* Debug messages are used for diagnostics, and are typically disabled
|
||||
* in production builds.
|
||||
|
||||
@@ -54,14 +54,7 @@ export class SendAccess extends Domain {
|
||||
async decrypt(key: SymmetricCryptoKey): Promise<SendAccessView> {
|
||||
const model = new SendAccessView(this);
|
||||
|
||||
await this.decryptObj(
|
||||
model,
|
||||
{
|
||||
name: null,
|
||||
},
|
||||
null,
|
||||
key,
|
||||
);
|
||||
await this.decryptObj<SendAccess, SendAccessView>(this, model, ["name"], null, key);
|
||||
|
||||
switch (this.type) {
|
||||
case SendType.File:
|
||||
|
||||
@@ -34,15 +34,13 @@ export class SendFile extends Domain {
|
||||
}
|
||||
|
||||
async decrypt(key: SymmetricCryptoKey): Promise<SendFileView> {
|
||||
const view = await this.decryptObj(
|
||||
return await this.decryptObj<SendFile, SendFileView>(
|
||||
this,
|
||||
new SendFileView(this),
|
||||
{
|
||||
fileName: null,
|
||||
},
|
||||
["fileName"],
|
||||
null,
|
||||
key,
|
||||
);
|
||||
return view;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<SendFile>) {
|
||||
|
||||
@@ -30,11 +30,10 @@ export class SendText extends Domain {
|
||||
}
|
||||
|
||||
decrypt(key: SymmetricCryptoKey): Promise<SendTextView> {
|
||||
return this.decryptObj(
|
||||
return this.decryptObj<SendText, SendTextView>(
|
||||
this,
|
||||
new SendTextView(this),
|
||||
{
|
||||
text: null,
|
||||
},
|
||||
["text"],
|
||||
null,
|
||||
key,
|
||||
);
|
||||
|
||||
@@ -87,15 +87,7 @@ export class Send extends Domain {
|
||||
// TODO: error?
|
||||
}
|
||||
|
||||
await this.decryptObj(
|
||||
model,
|
||||
{
|
||||
name: null,
|
||||
notes: null,
|
||||
},
|
||||
null,
|
||||
model.cryptoKey,
|
||||
);
|
||||
await this.decryptObj<Send, SendView>(this, model, ["name", "notes"], null, model.cryptoKey);
|
||||
|
||||
switch (this.type) {
|
||||
case SendType.File:
|
||||
|
||||
@@ -11,3 +11,4 @@ export type CipherId = Opaque<string, "CipherId">;
|
||||
export type SendId = Opaque<string, "SendId">;
|
||||
export type IndexedEntityId = Opaque<string, "IndexedEntityId">;
|
||||
export type SecurityTaskId = Opaque<string, "SecurityTaskId">;
|
||||
export type NotificationId = Opaque<string, "NotificationId">;
|
||||
|
||||
@@ -89,7 +89,7 @@ describe("buildCipherIcon", () => {
|
||||
|
||||
expect(iconDetails).toEqual({
|
||||
icon: "bwi-globe",
|
||||
image: undefined,
|
||||
image: null,
|
||||
fallbackImage: "",
|
||||
imageEnabled: false,
|
||||
});
|
||||
@@ -102,7 +102,7 @@ describe("buildCipherIcon", () => {
|
||||
|
||||
expect(iconDetails).toEqual({
|
||||
icon: "bwi-globe",
|
||||
image: undefined,
|
||||
image: null,
|
||||
fallbackImage: "",
|
||||
imageEnabled: true,
|
||||
});
|
||||
|
||||
@@ -2,9 +2,23 @@ import { Utils } from "../../platform/misc/utils";
|
||||
import { CipherType } from "../enums/cipher-type";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
|
||||
export function buildCipherIcon(iconsServerUrl: string, cipher: CipherView, showFavicon: boolean) {
|
||||
let icon;
|
||||
let image;
|
||||
export interface CipherIconDetails {
|
||||
imageEnabled: boolean;
|
||||
image: string | null;
|
||||
/**
|
||||
* @deprecated Fallback to `icon` instead which will default to "bwi-globe" if no other icon is applicable.
|
||||
*/
|
||||
fallbackImage: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export function buildCipherIcon(
|
||||
iconsServerUrl: string | null,
|
||||
cipher: CipherView,
|
||||
showFavicon: boolean,
|
||||
): CipherIconDetails {
|
||||
let icon: string = "bwi-globe";
|
||||
let image: string | null = null;
|
||||
let fallbackImage = "";
|
||||
const cardIcons: Record<string, string> = {
|
||||
Visa: "card-visa",
|
||||
@@ -18,6 +32,10 @@ export function buildCipherIcon(iconsServerUrl: string, cipher: CipherView, show
|
||||
RuPay: "card-ru-pay",
|
||||
};
|
||||
|
||||
if (iconsServerUrl == null) {
|
||||
showFavicon = false;
|
||||
}
|
||||
|
||||
switch (cipher.type) {
|
||||
case CipherType.Login:
|
||||
icon = "bwi-globe";
|
||||
@@ -53,9 +71,7 @@ export function buildCipherIcon(iconsServerUrl: string, cipher: CipherView, show
|
||||
try {
|
||||
image = `${iconsServerUrl}/${Utils.getHostname(hostnameUri)}/icon.png`;
|
||||
fallbackImage = "images/bwi-globe.png";
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignore error since the fallback icon will be shown if image is null.
|
||||
}
|
||||
}
|
||||
|
||||
21
libs/common/src/vault/models/api/cipher-permissions.api.ts
Normal file
21
libs/common/src/vault/models/api/cipher-permissions.api.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class CipherPermissionsApi extends BaseResponse {
|
||||
delete: boolean = false;
|
||||
restore: boolean = false;
|
||||
|
||||
constructor(data: any = null) {
|
||||
super(data);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
this.delete = this.getResponseProperty("Delete");
|
||||
this.restore = this.getResponseProperty("Restore");
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<CipherPermissionsApi>) {
|
||||
return Object.assign(new CipherPermissionsApi(), obj);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Jsonify } from "type-fest";
|
||||
|
||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||
import { CipherType } from "../../enums/cipher-type";
|
||||
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
|
||||
import { CipherResponse } from "../response/cipher.response";
|
||||
|
||||
import { AttachmentData } from "./attachment.data";
|
||||
@@ -21,6 +22,7 @@ export class CipherData {
|
||||
folderId: string;
|
||||
edit: boolean;
|
||||
viewPassword: boolean;
|
||||
permissions: CipherPermissionsApi;
|
||||
organizationUseTotp: boolean;
|
||||
favorite: boolean;
|
||||
revisionDate: string;
|
||||
@@ -51,6 +53,7 @@ export class CipherData {
|
||||
this.folderId = response.folderId;
|
||||
this.edit = response.edit;
|
||||
this.viewPassword = response.viewPassword;
|
||||
this.permissions = response.permissions;
|
||||
this.organizationUseTotp = response.organizationUseTotp;
|
||||
this.favorite = response.favorite;
|
||||
this.revisionDate = response.revisionDate;
|
||||
@@ -95,6 +98,8 @@ export class CipherData {
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<CipherData>) {
|
||||
return Object.assign(new CipherData(), obj);
|
||||
const result = Object.assign(new CipherData(), obj);
|
||||
result.permissions = CipherPermissionsApi.fromJSON(obj.permissions);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,11 +45,10 @@ export class Attachment extends Domain {
|
||||
context = "No Cipher Context",
|
||||
encKey?: SymmetricCryptoKey,
|
||||
): Promise<AttachmentView> {
|
||||
const view = await this.decryptObj(
|
||||
const view = await this.decryptObj<Attachment, AttachmentView>(
|
||||
this,
|
||||
new AttachmentView(this),
|
||||
{
|
||||
fileName: null,
|
||||
},
|
||||
["fileName"],
|
||||
orgId,
|
||||
encKey,
|
||||
"DomainType: Attachment; " + context,
|
||||
|
||||
@@ -44,16 +44,10 @@ export class Card extends Domain {
|
||||
context = "No Cipher Context",
|
||||
encKey?: SymmetricCryptoKey,
|
||||
): Promise<CardView> {
|
||||
return this.decryptObj(
|
||||
return this.decryptObj<Card, CardView>(
|
||||
this,
|
||||
new CardView(),
|
||||
{
|
||||
cardholderName: null,
|
||||
brand: null,
|
||||
number: null,
|
||||
expMonth: null,
|
||||
expYear: null,
|
||||
code: null,
|
||||
},
|
||||
["cardholderName", "brand", "number", "expMonth", "expYear", "code"],
|
||||
orgId,
|
||||
encKey,
|
||||
"DomainType: Card; " + context,
|
||||
|
||||
@@ -32,6 +32,7 @@ import { SecureNote } from "../../models/domain/secure-note";
|
||||
import { CardView } from "../../models/view/card.view";
|
||||
import { IdentityView } from "../../models/view/identity.view";
|
||||
import { LoginView } from "../../models/view/login.view";
|
||||
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
|
||||
|
||||
describe("Cipher DTO", () => {
|
||||
it("Convert from empty CipherData", () => {
|
||||
@@ -60,6 +61,7 @@ describe("Cipher DTO", () => {
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
key: null,
|
||||
permissions: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,6 +83,7 @@ describe("Cipher DTO", () => {
|
||||
notes: "EncryptedString",
|
||||
creationDate: "2022-01-01T12:00:00.000Z",
|
||||
deletedDate: null,
|
||||
permissions: new CipherPermissionsApi(),
|
||||
reprompt: CipherRepromptType.None,
|
||||
key: "EncryptedString",
|
||||
login: {
|
||||
@@ -155,6 +158,7 @@ describe("Cipher DTO", () => {
|
||||
localData: null,
|
||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
permissions: new CipherPermissionsApi(),
|
||||
reprompt: 0,
|
||||
key: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
login: {
|
||||
@@ -234,6 +238,7 @@ describe("Cipher DTO", () => {
|
||||
cipher.deletedDate = null;
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
cipher.key = mockEnc("EncKey");
|
||||
cipher.permissions = new CipherPermissionsApi();
|
||||
|
||||
const loginView = new LoginView();
|
||||
loginView.username = "username";
|
||||
@@ -276,6 +281,7 @@ describe("Cipher DTO", () => {
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: undefined,
|
||||
permissions: new CipherPermissionsApi(),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -303,6 +309,7 @@ describe("Cipher DTO", () => {
|
||||
secureNote: {
|
||||
type: SecureNoteType.Generic,
|
||||
},
|
||||
permissions: new CipherPermissionsApi(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -332,6 +339,7 @@ describe("Cipher DTO", () => {
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
key: { encryptedString: "EncKey", encryptionType: 0 },
|
||||
permissions: new CipherPermissionsApi(),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -359,6 +367,7 @@ describe("Cipher DTO", () => {
|
||||
cipher.secureNote = new SecureNote();
|
||||
cipher.secureNote.type = SecureNoteType.Generic;
|
||||
cipher.key = mockEnc("EncKey");
|
||||
cipher.permissions = new CipherPermissionsApi();
|
||||
|
||||
const keyService = mock<KeyService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
@@ -393,6 +402,7 @@ describe("Cipher DTO", () => {
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: undefined,
|
||||
permissions: new CipherPermissionsApi(),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -415,6 +425,7 @@ describe("Cipher DTO", () => {
|
||||
notes: "EncryptedString",
|
||||
creationDate: "2022-01-01T12:00:00.000Z",
|
||||
deletedDate: null,
|
||||
permissions: new CipherPermissionsApi(),
|
||||
reprompt: CipherRepromptType.None,
|
||||
card: {
|
||||
cardholderName: "EncryptedString",
|
||||
@@ -461,6 +472,7 @@ describe("Cipher DTO", () => {
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
key: { encryptedString: "EncKey", encryptionType: 0 },
|
||||
permissions: new CipherPermissionsApi(),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -486,6 +498,7 @@ describe("Cipher DTO", () => {
|
||||
cipher.deletedDate = null;
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
cipher.key = mockEnc("EncKey");
|
||||
cipher.permissions = new CipherPermissionsApi();
|
||||
|
||||
const cardView = new CardView();
|
||||
cardView.cardholderName = "cardholderName";
|
||||
@@ -528,6 +541,7 @@ describe("Cipher DTO", () => {
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: undefined,
|
||||
permissions: new CipherPermissionsApi(),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -550,6 +564,7 @@ describe("Cipher DTO", () => {
|
||||
notes: "EncryptedString",
|
||||
creationDate: "2022-01-01T12:00:00.000Z",
|
||||
deletedDate: null,
|
||||
permissions: new CipherPermissionsApi(),
|
||||
reprompt: CipherRepromptType.None,
|
||||
key: "EncKey",
|
||||
identity: {
|
||||
@@ -620,6 +635,7 @@ describe("Cipher DTO", () => {
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
key: { encryptedString: "EncKey", encryptionType: 0 },
|
||||
permissions: new CipherPermissionsApi(),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -645,6 +661,7 @@ describe("Cipher DTO", () => {
|
||||
cipher.deletedDate = null;
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
cipher.key = mockEnc("EncKey");
|
||||
cipher.permissions = new CipherPermissionsApi();
|
||||
|
||||
const identityView = new IdentityView();
|
||||
identityView.firstName = "firstName";
|
||||
@@ -687,6 +704,7 @@ describe("Cipher DTO", () => {
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: undefined,
|
||||
permissions: new CipherPermissionsApi(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
|
||||
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
|
||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||
import { CipherType } from "../../enums/cipher-type";
|
||||
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
|
||||
import { CipherData } from "../data/cipher.data";
|
||||
import { LocalData } from "../data/local.data";
|
||||
import { AttachmentView } from "../view/attachment.view";
|
||||
@@ -41,6 +42,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
organizationUseTotp: boolean;
|
||||
edit: boolean;
|
||||
viewPassword: boolean;
|
||||
permissions: CipherPermissionsApi;
|
||||
revisionDate: Date;
|
||||
localData: LocalData;
|
||||
login: Login;
|
||||
@@ -86,6 +88,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
} else {
|
||||
this.viewPassword = true; // Default for already synced Ciphers without viewPassword
|
||||
}
|
||||
this.permissions = obj.permissions;
|
||||
this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null;
|
||||
this.collectionIds = obj.collectionIds;
|
||||
this.localData = localData;
|
||||
@@ -156,12 +159,10 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
bypassValidation = false;
|
||||
}
|
||||
|
||||
await this.decryptObj(
|
||||
await this.decryptObj<Cipher, CipherView>(
|
||||
this,
|
||||
model,
|
||||
{
|
||||
name: null,
|
||||
notes: null,
|
||||
},
|
||||
["name", "notes"],
|
||||
this.organizationId,
|
||||
encKey,
|
||||
);
|
||||
@@ -248,6 +249,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : null;
|
||||
c.reprompt = this.reprompt;
|
||||
c.key = this.key?.encryptedString;
|
||||
c.permissions = this.permissions;
|
||||
|
||||
this.buildDataModel(this, c, {
|
||||
name: null,
|
||||
|
||||
@@ -54,41 +54,38 @@ export class Fido2Credential extends Domain {
|
||||
}
|
||||
|
||||
async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<Fido2CredentialView> {
|
||||
const view = await this.decryptObj(
|
||||
const view = await this.decryptObj<Fido2Credential, Fido2CredentialView>(
|
||||
this,
|
||||
new Fido2CredentialView(),
|
||||
{
|
||||
credentialId: null,
|
||||
keyType: null,
|
||||
keyAlgorithm: null,
|
||||
keyCurve: null,
|
||||
keyValue: null,
|
||||
rpId: null,
|
||||
userHandle: null,
|
||||
userName: null,
|
||||
rpName: null,
|
||||
userDisplayName: null,
|
||||
discoverable: null,
|
||||
},
|
||||
[
|
||||
"credentialId",
|
||||
"keyType",
|
||||
"keyAlgorithm",
|
||||
"keyCurve",
|
||||
"keyValue",
|
||||
"rpId",
|
||||
"userHandle",
|
||||
"userName",
|
||||
"rpName",
|
||||
"userDisplayName",
|
||||
],
|
||||
orgId,
|
||||
encKey,
|
||||
);
|
||||
|
||||
const { counter } = await this.decryptObj(
|
||||
{ counter: "" },
|
||||
const { counter } = await this.decryptObj<
|
||||
Fido2Credential,
|
||||
{
|
||||
counter: null,
|
||||
},
|
||||
orgId,
|
||||
encKey,
|
||||
);
|
||||
counter: string;
|
||||
}
|
||||
>(this, { counter: "" }, ["counter"], orgId, encKey);
|
||||
// Counter will end up as NaN if this fails
|
||||
view.counter = parseInt(counter);
|
||||
|
||||
const { discoverable } = await this.decryptObj(
|
||||
const { discoverable } = await this.decryptObj<Fido2Credential, { discoverable: string }>(
|
||||
this,
|
||||
{ discoverable: "" },
|
||||
{
|
||||
discoverable: null,
|
||||
},
|
||||
["discoverable"],
|
||||
orgId,
|
||||
encKey,
|
||||
);
|
||||
|
||||
@@ -37,12 +37,10 @@ export class Field extends Domain {
|
||||
}
|
||||
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<FieldView> {
|
||||
return this.decryptObj(
|
||||
return this.decryptObj<Field, FieldView>(
|
||||
this,
|
||||
new FieldView(this),
|
||||
{
|
||||
name: null,
|
||||
value: null,
|
||||
},
|
||||
["name", "value"],
|
||||
orgId,
|
||||
encKey,
|
||||
);
|
||||
|
||||
@@ -40,13 +40,7 @@ export class Folder extends Domain {
|
||||
}
|
||||
|
||||
decrypt(): Promise<FolderView> {
|
||||
return this.decryptObj(
|
||||
new FolderView(this),
|
||||
{
|
||||
name: null,
|
||||
},
|
||||
null,
|
||||
);
|
||||
return this.decryptObj<Folder, FolderView>(this, new FolderView(this), ["name"], null);
|
||||
}
|
||||
|
||||
async decryptWithKey(
|
||||
|
||||
@@ -68,28 +68,29 @@ export class Identity extends Domain {
|
||||
context: string = "No Cipher Context",
|
||||
encKey?: SymmetricCryptoKey,
|
||||
): Promise<IdentityView> {
|
||||
return this.decryptObj(
|
||||
return this.decryptObj<Identity, IdentityView>(
|
||||
this,
|
||||
new IdentityView(),
|
||||
{
|
||||
title: null,
|
||||
firstName: null,
|
||||
middleName: null,
|
||||
lastName: null,
|
||||
address1: null,
|
||||
address2: null,
|
||||
address3: null,
|
||||
city: null,
|
||||
state: null,
|
||||
postalCode: null,
|
||||
country: null,
|
||||
company: null,
|
||||
email: null,
|
||||
phone: null,
|
||||
ssn: null,
|
||||
username: null,
|
||||
passportNumber: null,
|
||||
licenseNumber: null,
|
||||
},
|
||||
[
|
||||
"title",
|
||||
"firstName",
|
||||
"middleName",
|
||||
"lastName",
|
||||
"address1",
|
||||
"address2",
|
||||
"address3",
|
||||
"city",
|
||||
"state",
|
||||
"postalCode",
|
||||
"country",
|
||||
"company",
|
||||
"email",
|
||||
"phone",
|
||||
"ssn",
|
||||
"username",
|
||||
"passportNumber",
|
||||
"licenseNumber",
|
||||
],
|
||||
orgId,
|
||||
encKey,
|
||||
"DomainType: Identity; " + context,
|
||||
|
||||
@@ -40,11 +40,10 @@ export class LoginUri extends Domain {
|
||||
context: string = "No Cipher Context",
|
||||
encKey?: SymmetricCryptoKey,
|
||||
): Promise<LoginUriView> {
|
||||
return this.decryptObj(
|
||||
return this.decryptObj<LoginUri, LoginUriView>(
|
||||
this,
|
||||
new LoginUriView(this),
|
||||
{
|
||||
uri: null,
|
||||
},
|
||||
["uri"],
|
||||
orgId,
|
||||
encKey,
|
||||
context,
|
||||
|
||||
@@ -60,13 +60,10 @@ export class Login extends Domain {
|
||||
context: string = "No Cipher Context",
|
||||
encKey?: SymmetricCryptoKey,
|
||||
): Promise<LoginView> {
|
||||
const view = await this.decryptObj(
|
||||
const view = await this.decryptObj<Login, LoginView>(
|
||||
this,
|
||||
new LoginView(this),
|
||||
{
|
||||
username: null,
|
||||
password: null,
|
||||
totp: null,
|
||||
},
|
||||
["username", "password", "totp"],
|
||||
orgId,
|
||||
encKey,
|
||||
`DomainType: Login; ${context}`,
|
||||
|
||||
@@ -27,11 +27,10 @@ export class Password extends Domain {
|
||||
}
|
||||
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<PasswordHistoryView> {
|
||||
return this.decryptObj(
|
||||
return this.decryptObj<Password, PasswordHistoryView>(
|
||||
this,
|
||||
new PasswordHistoryView(this),
|
||||
{
|
||||
password: null,
|
||||
},
|
||||
["password"],
|
||||
orgId,
|
||||
encKey,
|
||||
"DomainType: PasswordHistory",
|
||||
|
||||
@@ -38,13 +38,10 @@ export class SshKey extends Domain {
|
||||
context = "No Cipher Context",
|
||||
encKey?: SymmetricCryptoKey,
|
||||
): Promise<SshKeyView> {
|
||||
return this.decryptObj(
|
||||
return this.decryptObj<SshKey, SshKeyView>(
|
||||
this,
|
||||
new SshKeyView(),
|
||||
{
|
||||
privateKey: null,
|
||||
publicKey: null,
|
||||
keyFingerprint: null,
|
||||
},
|
||||
["privateKey", "publicKey", "keyFingerprint"],
|
||||
orgId,
|
||||
encKey,
|
||||
"DomainType: SshKey; " + context,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||
import { CardApi } from "../api/card.api";
|
||||
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
|
||||
import { FieldApi } from "../api/field.api";
|
||||
import { IdentityApi } from "../api/identity.api";
|
||||
import { LoginApi } from "../api/login.api";
|
||||
@@ -28,6 +29,7 @@ export class CipherResponse extends BaseResponse {
|
||||
favorite: boolean;
|
||||
edit: boolean;
|
||||
viewPassword: boolean;
|
||||
permissions: CipherPermissionsApi;
|
||||
organizationUseTotp: boolean;
|
||||
revisionDate: string;
|
||||
attachments: AttachmentResponse[];
|
||||
@@ -53,6 +55,7 @@ export class CipherResponse extends BaseResponse {
|
||||
} else {
|
||||
this.viewPassword = this.getResponseProperty("ViewPassword");
|
||||
}
|
||||
this.permissions = new CipherPermissionsApi(this.getResponseProperty("Permissions"));
|
||||
this.organizationUseTotp = this.getResponseProperty("OrganizationUseTotp");
|
||||
this.revisionDate = this.getResponseProperty("RevisionDate");
|
||||
this.collectionIds = this.getResponseProperty("CollectionIds");
|
||||
|
||||
@@ -6,6 +6,7 @@ import { InitializerKey } from "../../../platform/services/cryptography/initiali
|
||||
import { DeepJsonify } from "../../../types/deep-jsonify";
|
||||
import { CipherType, LinkedIdType } from "../../enums";
|
||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
|
||||
import { LocalData } from "../data/local.data";
|
||||
import { Cipher } from "../domain/cipher";
|
||||
|
||||
@@ -29,6 +30,7 @@ export class CipherView implements View, InitializerMetadata {
|
||||
type: CipherType = null;
|
||||
favorite = false;
|
||||
organizationUseTotp = false;
|
||||
permissions: CipherPermissionsApi = new CipherPermissionsApi();
|
||||
edit = false;
|
||||
viewPassword = true;
|
||||
localData: LocalData;
|
||||
@@ -63,6 +65,7 @@ export class CipherView implements View, InitializerMetadata {
|
||||
this.organizationUseTotp = c.organizationUseTotp;
|
||||
this.edit = c.edit;
|
||||
this.viewPassword = c.viewPassword;
|
||||
this.permissions = c.permissions;
|
||||
this.type = c.type;
|
||||
this.localData = c.localData;
|
||||
this.collectionIds = c.collectionIds;
|
||||
|
||||
@@ -8,6 +8,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec";
|
||||
import { ConfigService } from "../../platform/abstractions/config/config.service";
|
||||
import { CipherPermissionsApi } from "../models/api/cipher-permissions.api";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
|
||||
import {
|
||||
@@ -20,6 +22,7 @@ describe("CipherAuthorizationService", () => {
|
||||
|
||||
const mockCollectionService = mock<CollectionService>();
|
||||
const mockOrganizationService = mock<OrganizationService>();
|
||||
const mockConfigService = mock<ConfigService>();
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let mockAccountService: FakeAccountService;
|
||||
|
||||
@@ -28,10 +31,12 @@ describe("CipherAuthorizationService", () => {
|
||||
organizationId: string | null,
|
||||
collectionIds: string[],
|
||||
edit: boolean = true,
|
||||
permissions: CipherPermissionsApi = new CipherPermissionsApi(),
|
||||
) => ({
|
||||
organizationId,
|
||||
collectionIds,
|
||||
edit,
|
||||
permissions,
|
||||
});
|
||||
|
||||
const createMockCollection = (id: string, manage: boolean) => ({
|
||||
@@ -63,7 +68,78 @@ describe("CipherAuthorizationService", () => {
|
||||
mockCollectionService,
|
||||
mockOrganizationService,
|
||||
mockAccountService,
|
||||
mockConfigService,
|
||||
);
|
||||
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
});
|
||||
|
||||
describe("canRestoreCipher$", () => {
|
||||
it("should return true if isAdminConsoleAction and cipher is unassigned", (done) => {
|
||||
const cipher = createMockCipher("org1", []) as CipherView;
|
||||
const organization = createMockOrganization({ canEditUnassignedCiphers: true });
|
||||
mockOrganizationService.organizations$.mockReturnValue(
|
||||
of([organization]) as Observable<Organization[]>,
|
||||
);
|
||||
|
||||
cipherAuthorizationService.canRestoreCipher$(cipher, true).subscribe((result) => {
|
||||
expect(result).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return true if isAdminConsleAction and user can edit all ciphers in the org", (done) => {
|
||||
const cipher = createMockCipher("org1", ["col1"]) as CipherView;
|
||||
const organization = createMockOrganization({ canEditAllCiphers: true });
|
||||
mockOrganizationService.organizations$.mockReturnValue(
|
||||
of([organization]) as Observable<Organization[]>,
|
||||
);
|
||||
|
||||
cipherAuthorizationService.canRestoreCipher$(cipher, true).subscribe((result) => {
|
||||
expect(result).toBe(true);
|
||||
expect(mockOrganizationService.organizations$).toHaveBeenCalledWith(mockUserId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false if isAdminConsoleAction is true but user does not have permission to edit unassigned ciphers", (done) => {
|
||||
const cipher = createMockCipher("org1", []) as CipherView;
|
||||
const organization = createMockOrganization({ canEditUnassignedCiphers: false });
|
||||
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
|
||||
|
||||
cipherAuthorizationService.canRestoreCipher$(cipher, true).subscribe((result) => {
|
||||
expect(result).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false if cipher.permission.restore is false and is not an admin action", (done) => {
|
||||
const cipher = createMockCipher("org1", [], true, {
|
||||
restore: false,
|
||||
} as CipherPermissionsApi) as CipherView;
|
||||
const organization = createMockOrganization();
|
||||
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
|
||||
|
||||
cipherAuthorizationService.canRestoreCipher$(cipher, false).subscribe((result) => {
|
||||
expect(result).toBe(false);
|
||||
expect(mockCollectionService.decryptedCollectionViews$).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return true if cipher.permission.restore is true and is not an admin action", (done) => {
|
||||
const cipher = createMockCipher("org1", [], true, {
|
||||
restore: true,
|
||||
} as CipherPermissionsApi) as CipherView;
|
||||
const organization = createMockOrganization();
|
||||
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
|
||||
|
||||
cipherAuthorizationService.canRestoreCipher$(cipher, false).subscribe((result) => {
|
||||
expect(result).toBe(true);
|
||||
expect(mockCollectionService.decryptedCollectionViews$).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("canDeleteCipher$", () => {
|
||||
@@ -213,6 +289,34 @@ describe("CipherAuthorizationService", () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return true if feature flag enabled and cipher.permissions.delete is true", (done) => {
|
||||
const cipher = createMockCipher("org1", [], true, {
|
||||
delete: true,
|
||||
} as CipherPermissionsApi) as CipherView;
|
||||
const organization = createMockOrganization();
|
||||
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
cipherAuthorizationService.canDeleteCipher$(cipher, [], false).subscribe((result) => {
|
||||
expect(result).toBe(true);
|
||||
expect(mockCollectionService.decryptedCollectionViews$).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false if feature flag enabled and cipher.permissions.delete is false", (done) => {
|
||||
const cipher = createMockCipher("org1", []) as CipherView;
|
||||
const organization = createMockOrganization();
|
||||
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
cipherAuthorizationService.canDeleteCipher$(cipher, [], false).subscribe((result) => {
|
||||
expect(result).toBe(false);
|
||||
expect(mockCollectionService.decryptedCollectionViews$).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("canCloneCipher$", () => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { map, Observable, of, shareReplay, switchMap } from "rxjs";
|
||||
import { combineLatest, map, Observable, of, shareReplay, switchMap } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { getUserId } from "../../auth/services/account.service";
|
||||
import { FeatureFlag } from "../../enums/feature-flag.enum";
|
||||
import { Cipher } from "../models/domain/cipher";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
|
||||
@@ -28,12 +29,25 @@ export abstract class CipherAuthorizationService {
|
||||
*
|
||||
* @returns {Observable<boolean>} - An observable that emits a boolean value indicating if the user can delete the cipher.
|
||||
*/
|
||||
canDeleteCipher$: (
|
||||
abstract canDeleteCipher$: (
|
||||
cipher: CipherLike,
|
||||
allowedCollections?: CollectionId[],
|
||||
isAdminConsoleAction?: boolean,
|
||||
) => Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Determines if the user can restore the specified cipher.
|
||||
*
|
||||
* @param {CipherLike} cipher - The cipher object to evaluate for restore permissions.
|
||||
* @param {boolean} isAdminConsoleAction - Optional. A flag indicating if the action is being performed from the admin console.
|
||||
*
|
||||
* @returns {Observable<boolean>} - An observable that emits a boolean value indicating if the user can restore the cipher.
|
||||
*/
|
||||
abstract canRestoreCipher$: (
|
||||
cipher: CipherLike,
|
||||
isAdminConsoleAction?: boolean,
|
||||
) => Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Determines if the user can clone the specified cipher.
|
||||
*
|
||||
@@ -42,7 +56,10 @@ export abstract class CipherAuthorizationService {
|
||||
*
|
||||
* @returns {Observable<boolean>} - An observable that emits a boolean value indicating if the user can clone the cipher.
|
||||
*/
|
||||
canCloneCipher$: (cipher: CipherLike, isAdminConsoleAction?: boolean) => Observable<boolean>;
|
||||
abstract canCloneCipher$: (
|
||||
cipher: CipherLike,
|
||||
isAdminConsoleAction?: boolean,
|
||||
) => Observable<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,13 +70,16 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
|
||||
private collectionService: CollectionService,
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
private organization$ = (cipher: CipherLike) =>
|
||||
this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) => this.organizationService.organizations$(account?.id)),
|
||||
getUserId,
|
||||
switchMap((userId) => this.organizationService.organizations$(userId)),
|
||||
map((orgs) => orgs.find((org) => org.id === cipher.organizationId)),
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* {@link CipherAuthorizationService.canDeleteCipher$}
|
||||
@@ -69,12 +89,11 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
|
||||
allowedCollections?: CollectionId[],
|
||||
isAdminConsoleAction?: boolean,
|
||||
): Observable<boolean> {
|
||||
if (cipher.organizationId == null) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
return this.organization$(cipher).pipe(
|
||||
switchMap((organization) => {
|
||||
return combineLatest([
|
||||
this.organization$(cipher),
|
||||
this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion),
|
||||
]).pipe(
|
||||
switchMap(([organization, featureFlagEnabled]) => {
|
||||
if (isAdminConsoleAction) {
|
||||
// If the user is an admin, they can delete an unassigned cipher
|
||||
if (!cipher.collectionIds || cipher.collectionIds.length === 0) {
|
||||
@@ -86,6 +105,14 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
|
||||
}
|
||||
}
|
||||
|
||||
if (featureFlagEnabled) {
|
||||
return of(cipher.permissions.delete);
|
||||
}
|
||||
|
||||
if (cipher.organizationId == null) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
return this.collectionService
|
||||
.decryptedCollectionViews$(cipher.collectionIds as CollectionId[])
|
||||
.pipe(
|
||||
@@ -93,7 +120,7 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
|
||||
const shouldFilter = allowedCollections?.some(Boolean);
|
||||
|
||||
const collections = shouldFilter
|
||||
? allCollections.filter((c) => allowedCollections.includes(c.id as CollectionId))
|
||||
? allCollections.filter((c) => allowedCollections?.includes(c.id as CollectionId))
|
||||
: allCollections;
|
||||
|
||||
return collections.some((collection) => collection.manage);
|
||||
@@ -103,6 +130,29 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@link CipherAuthorizationService.canRestoreCipher$}
|
||||
*/
|
||||
canRestoreCipher$(cipher: CipherLike, isAdminConsoleAction?: boolean): Observable<boolean> {
|
||||
return this.organization$(cipher).pipe(
|
||||
map((organization) => {
|
||||
if (isAdminConsoleAction) {
|
||||
// If the user is an admin, they can restore an unassigned cipher
|
||||
if (!cipher.collectionIds || cipher.collectionIds.length === 0) {
|
||||
return organization?.canEditUnassignedCiphers === true;
|
||||
}
|
||||
|
||||
if (organization?.canEditAllCiphers) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return cipher.permissions.restore;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link CipherAuthorizationService.canCloneCipher$}
|
||||
*/
|
||||
@@ -116,6 +166,7 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
|
||||
// Admins and custom users can always clone when in the Admin Console
|
||||
if (
|
||||
isAdminConsoleAction &&
|
||||
organization &&
|
||||
(organization.isAdmin || organization.permissions?.editAnyCollection)
|
||||
) {
|
||||
return of(true);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user