1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-07 04:03:29 +00:00

refactor(change-password-component): Change Password Update [18720] - Fixed up policy service to be made more clear.

This commit is contained in:
Patrick Pimentel
2025-06-02 21:09:28 -04:00
parent 927d1e9fa3
commit db16cf0115
6 changed files with 112 additions and 64 deletions

View File

@@ -104,9 +104,9 @@ export class WebLoginComponentService
if (
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
) {
// Properly error if we don't have an org invite with
// Properly error if we don't have an org invite
enforcedPasswordPolicyOptions = await firstValueFrom(
this.policyService.masterPasswordPolicyOptions$(orgInvite.userId, policies),
this.policyService.masterPasswordPolicyOptionsPriorToSync$(policies),
);
} else {
enforcedPasswordPolicyOptions = await firstValueFrom(

View File

@@ -3,8 +3,6 @@
import { Params } from "@angular/router";
import { Jsonify } from "type-fest";
import { UserId } from "@bitwarden/common/types/guid";
export class OrganizationInvite {
email: string;
initOrganization: boolean;
@@ -14,7 +12,6 @@ export class OrganizationInvite {
organizationName: string;
organizationUserId: string;
token: string;
userId: UserId;
static fromJSON(json: Jsonify<OrganizationInvite>): OrganizationInvite | null {
if (json == null) {
@@ -38,7 +35,6 @@ export class OrganizationInvite {
organizationName: params.organizationName,
organizationUserId: params.organizationUserId,
token: params.token,
userId: params.userId,
});
}
}

View File

@@ -85,6 +85,8 @@ export class LoginComponent implements OnInit, OnDestroy {
isKnownDevice = false;
loginUiState: LoginUiState = LoginUiState.EMAIL_ENTRY;
passwordPoliciesFromOrgInvite?: Policy[];
formGroup = this.formBuilder.group(
{
email: ["", [Validators.required, Validators.email]],
@@ -238,8 +240,15 @@ export class LoginComponent implements OnInit, OnDestroy {
this.loginComponentService.getOrgPoliciesFromOrgInvite
) {
const orgPoliciesFromInvite = await this.loginComponentService.getOrgPoliciesFromOrgInvite();
const orgPolicies = orgPoliciesFromInvite?.enforcedPasswordPolicyOptions ?? undefined;
credentials = new PasswordLoginCredentials(email, masterPassword, undefined, orgPolicies);
const orgMasterPasswordPolicyOptions =
orgPoliciesFromInvite?.enforcedPasswordPolicyOptions ?? undefined;
this.passwordPoliciesFromOrgInvite = orgPoliciesFromInvite?.policies;
credentials = new PasswordLoginCredentials(
email,
masterPassword,
undefined,
orgMasterPasswordPolicyOptions,
);
} else {
credentials = new PasswordLoginCredentials(email, masterPassword);
}
@@ -329,8 +338,13 @@ export class LoginComponent implements OnInit, OnDestroy {
// The AuthGuard will handle routing to update-temp-password based on state
if (
!(await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor))
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
) {
// Check if we had a
if (this.passwordPoliciesFromOrgInvite) {
await this.setPoliciesIntoState(authResult.userId, this.passwordPoliciesFromOrgInvite);
}
} else {
// TODO: PM-18269 - evaluate if we can combine this with the
// password evaluation done in the password login strategy.
// If there's an existing org invite, use it to get the org's password policies

View File

@@ -98,6 +98,8 @@ export class PasswordLoginStrategy extends LoginStrategy {
await this.buildDeviceRequest(),
);
// TODO: add master password policy conditions to the cache so that it is available after 2fa for password evaluation
this.cache.next(data);
const [authResult, identityResponse] = await this.startLogIn();

View File

@@ -44,12 +44,37 @@ export abstract class PolicyService {
* @param policies The policies to be evaluated; if null or undefined, it will default to using policies from sync data.
* @returns a set of options which represent the minimum Master Password settings that the user must
* comply with in order to comply with **all** applicable Master Password policies.
*
* @deprecated Deprecating because the parameters can be made more strict and clear.
*/
abstract masterPasswordPolicyOptions$: (
userId: UserId,
policies?: Policy[],
) => Observable<MasterPasswordPolicyOptions | undefined>;
/**
* Combines all Master Password policies that apply to the user.
* Used for after a login / sync has occurred and the policy state has been set in state.
* @param userId The user against whom the policy needs to be enforced.
* @returns a set of options which represent the minimum Master Password settings that the user must
* comply with in order to comply with **all** applicable Master Password policies.
*/
abstract masterPasswordPolicyOptionsByUserId$: (
userId: UserId,
) => Observable<MasterPasswordPolicyOptions | undefined>;
/**
* Combines all Master Password policies that apply to the user that comes from the policies that
* are passed to this function.
* This would be used prior to obtaining a user id, such as before login for an org invite.
* @param policies The policies to be evaluated; if null or undefined, it will default to using policies from sync data.
* @returns a set of options which represent the minimum Master Password settings that the user must
* comply with in order to comply with **all** applicable Master Password policies.
*/
abstract masterPasswordPolicyOptionsPriorToSync$: (
policies: Policy[],
) => Observable<MasterPasswordPolicyOptions | undefined>;
/**
* Evaluates whether a proposed Master Password complies with all Master Password policies that apply to the user.
*/

View File

@@ -87,63 +87,19 @@ export class DefaultPolicyService implements PolicyService {
policies?: Policy[],
): Observable<MasterPasswordPolicyOptions | undefined> {
const policies$ = policies ? of(policies) : this.policies$(userId);
return policies$.pipe(
map((obsPolicies) => {
let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined;
const filteredPolicies =
obsPolicies.filter((p) => p.type === PolicyType.MasterPassword) ?? [];
return policies$.pipe(map((obsPolicies) => this.policyMapping(obsPolicies)));
}
if (filteredPolicies.length === 0) {
return;
}
masterPasswordPolicyOptionsByUserId$(
userId: UserId,
): Observable<MasterPasswordPolicyOptions | undefined> {
return this.policies$(userId).pipe(map((obsPolicies) => this.policyMapping(obsPolicies)));
}
filteredPolicies.forEach((currentPolicy) => {
if (!currentPolicy.enabled || !currentPolicy.data) {
return;
}
if (!enforcedOptions) {
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;
}),
);
masterPasswordPolicyOptionsPriorToSync$(
policies: Policy[],
): Observable<MasterPasswordPolicyOptions | undefined> {
return of(policies).pipe(map((obsPolicies) => this.policyMapping(obsPolicies)));
}
evaluateMasterPassword(
@@ -241,4 +197,59 @@ export class DefaultPolicyService implements PolicyService {
return organization.canManagePolicies;
}
}
private policyMapping(obsPolicies: Policy[]): MasterPasswordPolicyOptions | undefined {
let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined;
const filteredPolicies = obsPolicies.filter((p) => p.type === PolicyType.MasterPassword) ?? [];
if (filteredPolicies.length === 0) {
return;
}
filteredPolicies.forEach((currentPolicy) => {
if (!currentPolicy.enabled || !currentPolicy.data) {
return;
}
if (!enforcedOptions) {
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;
}
}