1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-25 00:53:22 +00:00

fix(change-password-component): Change Password Update [18720] - Took org invite state out of service and made it accessible.

This commit is contained in:
Patrick Pimentel
2025-06-22 20:31:20 -04:00
parent 815f379c24
commit 735a114baa
33 changed files with 417 additions and 317 deletions

View File

@@ -51,13 +51,24 @@ export abstract class PolicyService {
) => Observable<MasterPasswordPolicyOptions | undefined>;
/**
* Combines all Master Password policies that are passed in.
* Combines all Master Password policies that are passed in and returns
* back the strongest combination of all the policies in the form of a
* MasterPasswordPolicyOptions.
* @param policies
*/
abstract combineMasterPasswordPolicies(
abstract combinePoliciesIntoMasterPasswordPolicyOptions(
policies: Policy[],
): MasterPasswordPolicyOptions | undefined;
/**
* Takes an arbitrary amount of Master Password Policy options in any form and merges them
* together using the strictest combination of all of them.
* @param masterPasswordPolicyOptions
*/
abstract combineMasterPasswordPolicyOptions(
...masterPasswordPolicyOptions: MasterPasswordPolicyOptions[]
): MasterPasswordPolicyOptions | undefined;
/**
* Evaluates whether a proposed Master Password complies with all Master Password policies that apply to the user.
*/

View File

@@ -19,16 +19,7 @@ export class MasterPasswordPolicyOptions extends Domain {
enforceOnLogin = false;
static fromResponse(policy: MasterPasswordPolicyResponse): MasterPasswordPolicyOptions {
// Check if the policy is null or if all the values in the response object is null.
// Exclude the response object because the MasterPasswordPolicyResponse extends
// BaseResponse and we should omit that when checking for null values. Doing this
// programmatically makes this less brittle for future contract changes.
if (
policy == null ||
Object.entries(policy)
.filter(([key]) => key !== "response")
.every(([, value]) => value == null)
) {
if (policy == null) {
return null;
}
const options = new MasterPasswordPolicyOptions();

View File

@@ -87,10 +87,14 @@ export class DefaultPolicyService implements PolicyService {
policies?: Policy[],
): Observable<MasterPasswordPolicyOptions | undefined> {
const policies$ = policies ? of(policies) : this.policies$(userId);
return policies$.pipe(map((obsPolicies) => this.combineMasterPasswordPolicies(obsPolicies)));
return policies$.pipe(
map((obsPolicies) => this.combinePoliciesIntoMasterPasswordPolicyOptions(obsPolicies)),
);
}
combineMasterPasswordPolicies(policies: Policy[]): MasterPasswordPolicyOptions | undefined {
combinePoliciesIntoMasterPasswordPolicyOptions(
policies: Policy[],
): MasterPasswordPolicyOptions | undefined {
let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined;
const filteredPolicies = policies.filter((p) => p.type === PolicyType.MasterPassword) ?? [];
@@ -100,51 +104,35 @@ export class DefaultPolicyService implements PolicyService {
filteredPolicies.forEach((currentPolicy) => {
if (!currentPolicy.enabled || !currentPolicy.data) {
return;
return undefined;
}
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;
}
this.mergeMasterPasswordPolicyOptions(enforcedOptions, currentPolicy.data);
});
return enforcedOptions;
}
combineMasterPasswordPolicyOptions(
...policies: MasterPasswordPolicyOptions[]
): MasterPasswordPolicyOptions | undefined {
let combinedOptions: MasterPasswordPolicyOptions | undefined = undefined;
policies.forEach((currentOptions) => {
if (!combinedOptions) {
combinedOptions = new MasterPasswordPolicyOptions();
}
this.mergeMasterPasswordPolicyOptions(combinedOptions, currentOptions);
});
return combinedOptions;
}
evaluateMasterPassword(
passwordStrength: number,
newPassword: string,
@@ -240,4 +228,26 @@ export class DefaultPolicyService implements PolicyService {
return organization.canManagePolicies;
}
}
private mergeMasterPasswordPolicyOptions(
target: MasterPasswordPolicyOptions | undefined,
source: MasterPasswordPolicyOptions | undefined,
) {
if (!target) {
target = new MasterPasswordPolicyOptions();
}
if (source) {
target.minComplexity = Math.max(
target.minComplexity,
source.minComplexity ?? target.minComplexity,
);
target.minLength = Math.max(target.minLength, source.minLength ?? target.minLength);
target.requireUpper = target.requireUpper || source.requireUpper;
target.requireLower = target.requireLower || source.requireLower;
target.requireNumbers = target.requireNumbers || source.requireNumbers;
target.requireSpecial = target.requireSpecial || source.requireSpecial;
target.enforceOnLogin = target.enforceOnLogin || source.enforceOnLogin;
}
}
}

View File

@@ -0,0 +1,37 @@
import { firstValueFrom } from "rxjs";
import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite";
import { ORGANIZATION_INVITE } from "@bitwarden/common/auth/services/organization-invite/organization-invite-state";
import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state";
export class OrganizationInviteService {
private organizationInvitationState: GlobalState<OrganizationInvite | null>;
constructor(private readonly globalStateProvider: GlobalStateProvider) {
this.organizationInvitationState = this.globalStateProvider.get(ORGANIZATION_INVITE);
}
/**
* Returns the currently stored organization invite
*/
async getOrganizationInvite(): Promise<OrganizationInvite | null> {
return await firstValueFrom(this.organizationInvitationState.state$);
}
/**
* Stores a new organization invite
* @param invite an organization invite
* @throws if the invite is nullish
*/
async setOrganizationInvitation(invite: OrganizationInvite): Promise<void> {
if (invite == null) {
throw new Error("Invite cannot be null. Use clearOrganizationInvitation instead.");
}
await this.organizationInvitationState.update(() => invite);
}
/** Clears the currently stored organization invite */
async clearOrganizationInvitation(): Promise<void> {
await this.organizationInvitationState.update(() => null);
}
}

View File

@@ -0,0 +1,13 @@
import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite";
import { KeyDefinition, ORGANIZATION_INVITE_DISK } from "@bitwarden/common/platform/state";
// We're storing the organization invite for 2 reasons:
// 1. If the org requires a MP policy check, we need to keep track that the user has already been redirected when they return.
// 2. The MP policy check happens on login/register flows, we need to store the token to retrieve the policies then.
export const ORGANIZATION_INVITE = new KeyDefinition<OrganizationInvite | null>(
ORGANIZATION_INVITE_DISK,
"organizationInvite",
{
deserializer: (invite) => (invite ? OrganizationInvite.fromJSON(invite) : null),
},
);

View File

@@ -0,0 +1,20 @@
import { Jsonify } from "type-fest";
export class OrganizationInvite {
email?: string;
initOrganization?: boolean;
orgSsoIdentifier?: string;
orgUserHasExistingUser?: boolean;
organizationId?: string;
organizationName?: string;
organizationUserId?: string;
token?: string;
static fromJSON(json: Jsonify<OrganizationInvite>): OrganizationInvite | null {
if (json == null) {
return null;
}
return Object.assign(new OrganizationInvite(), json);
}
}