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:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user