1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

feat(change-password): [PM-18720] (#5319) Change Password Implementation for Non Dialog Cases (#15319)

* feat(change-password-component): Change Password Update [18720] - Very close to complete.

* fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Removed temp code to force the state I need to verify correctness.

* fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Recover account working with change password component.

* fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Made code more dry.

* fix(change-password-component): Change Password Update [18720] - Updates to routing and the extension. Extension is still a wip.

* fix(change-password-component): Change Password Update [18720] - Extension routing changes.

* feat(change-password-component): Change Password Update [18720] - More extension work

* feat(change-password-component): Change Password Update [18720] - Pausing work for now while we wait for product to hear back.

* feat(change-password-component): Change Password Update [18720] - Removed duplicated anon layouts.

* feat(change-password-component): Change Password Update [18720] - Tidied up code.

* feat(change-password-component): Change Password Update [18720] - Small fixes to the styling

* feat(change-password-component): Change Password Update [18720] - Adding more content for the routing.

* feat(change-password-component): Change Password Update [18720] - Removed circular loop for now.

* feat(change-password-component): Change Password Update [18720] - Made comments regarding the change password routing complexities with change-password and auth guard.

* feat(change-password-component): Change Password Update [18720] - Undid some changes because they will be conflicts later on.

* feat(change-password-component): Change Password Update [18720] - Small directive change.

* feat(change-password-component): Change Password Update [18720] - Small changes and added some clarification on where I'm blocked

* feat(change-password-component): Change Password Update [18720] - Org invite is seemingly working, found one bug to iron out.

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

* docs(change-password-component): Change Password Update [18720] - Updated documentation.

* refactor(change-password-component): Change Password Update [18720] - Routing changes and policy service changes.

* fix(change-password-component): Change Password Update [18720] - Wrapping up changes.

* feat(change-password-component): Change Password Update [18720] - Should be working fully

* feat(change-password-component): Change Password Update [18720] - Found a bug, working on password policy being present on login.

* feat(change-password-component): Change Password Update [18720] - Turned on auth guard on other clients for change-password route.

* feat(change-password-component): Change Password Update [18720] - Committing intermediate changes.

* feat(change-password-component): Change Password Update [18720] - The master password policy endpoint has been added! Should be working. Testing now.

* feat(change-password-component): Change Password Update [18720] - Minor fixes.

* feat(change-password-component): Change Password Update [18720] - Undid naming change.

* feat(change-password-component): Change Password Update [18720] - Removed comment.

* feat(change-password-component): Change Password Update [18720] - Removed unneeded code.

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

* fix(change-password-component): Change Password Update [18720] - Small changes.

* fix(change-password-component): Change Password Update [18720] - Split up org invite service into client specific implementations and have them injected into clients properly

* feat(change-password-component): Change Password Update [18720] - Stopping work and going to switch to a new branch to pare down some of the solutions that were made to get this over the finish line

* feat(change-password-component): Change Password Update [18720] - Started to remove functionality in the login.component and the password login strategy.

* feat(change-password-component): Change Password Update [18720] - Removed more unneded changes.

* feat(change-password-component): Change Password Update [18720] - Change password clearing state working properly.

* fix(change-password-component): Change Password Update [18720] - Added docs and moved web implementation.

* comments(change-password-component): Change Password Update [18720] - Added more notes.

* test(change-password-component): Change Password Update [18720] - Added in tests for policy service.

* comment(change-password-component): Change Password Update [18720] - Updated doc with correct ticket number.

* comment(change-password-component): Change Password Update [18720] - Fixed doc.

* test(change-password-component): Change Password Update [18720] - Fixed tests.

* test(change-password-component): Change Password Update [18720] - Fixed linting errors. Have more tests to fix.

* test(change-password-component): Change Password Update [18720] - Added back in ignore for typesafety.

* fix(change-password-component): Change Password Update [18720] - Fixed other type issues.

* test(change-password-component): Change Password Update [18720] - Fixed tests.

* test(change-password-component): Change Password Update [18720] - Fixed more tests.

* test(change-password-component): Change Password Update [18720] - Fixed tiny duplicate code.

* fix(change-password-component): Change Password Update [18720] - Fixed desktop component.

* fix(change-password-component): Change Password Update [18720] - Removed unused code

* fix(change-password-component): Change Password Update [18720] - Fixed locales.

* fix(change-password-component): Change Password Update [18720] - Removed tracing.

* fix(change-password-component): Change Password Update [18720] - Removed duplicative services module entry.

* fix(change-password-component): Change Password Update [18720] - Added comment.

* fix(change-password-component): Change Password Update [18720] - Fixed unneeded call in two factor to get user id.

* fix(change-password-component): Change Password Update [18720] - Fixed a couple of tiny things.

* fix(change-password-component): Change Password Update [18720] - Added comment for later fix.

* fix(change-password-component): Change Password Update [18720] - Fixed linting error.

* PM-18720 - AuthGuard - move call to get isChangePasswordFlagOn down after other conditions for efficiency.

* PM-18720 - PasswordLoginStrategy tests - test new feature flagged combine org invite policies logic for weak password evaluation.

* PM-18720 - CLI - fix dep issue

* PM-18720 - ChangePasswordComp - extract change password warning up out of input password component

* PM-18720 - InputPassword - remove unused dependency.

* PM-18720 - ChangePasswordComp - add callout dep

* PM-18720 - Revert all anon-layout changes

* PM-18720 - Anon Layout - finish reverting changes.

* PM-18720 - WIP move of change password out of libs/auth

* PM-18720 - Clean up remaining imports from moving change password out of libs/auth

* PM-18720 - Add change-password barrel file for better import grouping

* PM-18720 - Change Password comp - restore maxWidth

* PM-18720 - After merge, fix errors

* PM-18720 - Desktop - fix api service import

* PM-18720 - NDV - fix routing.

* PM-18720 - Change Password Comp - add logout service todo

* PM-18720 - PasswordSettings - per feedback, component is already feature flagged behind PM16117_ChangeExistingPasswordRefactor so we can just delete the replaced callout (new text is in change-password comp)

* PM-18720 - Routing Modules - properly flag new component behind feature flag.

* PM-18720 - SSO Login Strategy - fix config service import since it is now in shared deps from main merge.

* PM-18720 - Fix SSO login strategy tests

* PM-18720 - Default Policy Service - address AC PR feedback

---------

Co-authored-by: Jared Snider <jsnider@bitwarden.com>
Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
Patrick-Pimentel-Bitwarden
2025-07-10 09:08:25 -04:00
committed by GitHub
parent ec015bd253
commit 1f60bcdcc0
70 changed files with 1301 additions and 495 deletions

View File

@@ -50,6 +50,25 @@ export abstract class PolicyService {
policies?: Policy[],
) => Observable<MasterPasswordPolicyOptions | undefined>;
/**
* 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 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

@@ -536,6 +536,152 @@ describe("PolicyService", () => {
});
});
describe("combinePoliciesIntoMasterPasswordPolicyOptions", () => {
let policyService: DefaultPolicyService;
let stateProvider: FakeStateProvider;
let organizationService: MockProxy<OrganizationService>;
beforeEach(() => {
stateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
organizationService = mock<OrganizationService>();
policyService = new DefaultPolicyService(stateProvider, organizationService);
});
it("returns undefined when there are no policies", () => {
const result = policyService.combinePoliciesIntoMasterPasswordPolicyOptions([]);
expect(result).toBeUndefined();
});
it("returns options for a single policy", () => {
const masterPasswordPolicyRequirements = {
minComplexity: 3,
minLength: 10,
requireUpper: true,
};
const policies = [
new Policy(
policyData(
"1",
"org1",
PolicyType.MasterPassword,
true,
masterPasswordPolicyRequirements,
),
),
];
const result = policyService.combinePoliciesIntoMasterPasswordPolicyOptions(policies);
expect(result).toEqual({
minComplexity: 3,
minLength: 10,
requireUpper: true,
requireLower: false,
requireNumbers: false,
requireSpecial: false,
enforceOnLogin: false,
});
});
it("merges options from multiple policies", () => {
const masterPasswordPolicyRequirements1 = {
minComplexity: 3,
minLength: 10,
requireUpper: true,
};
const masterPasswordPolicyRequirements2 = { minComplexity: 5, requireNumbers: true };
const policies = [
new Policy(
policyData(
"1",
"org1",
PolicyType.MasterPassword,
true,
masterPasswordPolicyRequirements1,
),
),
new Policy(
policyData(
"2",
"org2",
PolicyType.MasterPassword,
true,
masterPasswordPolicyRequirements2,
),
),
];
const result = policyService.combinePoliciesIntoMasterPasswordPolicyOptions(policies);
expect(result).toEqual({
minComplexity: 5,
minLength: 10,
requireUpper: true,
requireLower: false,
requireNumbers: true,
requireSpecial: false,
enforceOnLogin: false,
});
});
it("ignores disabled policies", () => {
const masterPasswordPolicyRequirements = {
minComplexity: 3,
minLength: 10,
requireUpper: true,
};
const policies = [
new Policy(
policyData(
"1",
"org1",
PolicyType.MasterPassword,
false,
masterPasswordPolicyRequirements,
),
),
];
const result = policyService.combinePoliciesIntoMasterPasswordPolicyOptions(policies);
expect(result).toBeUndefined();
});
it("ignores policies with no data", () => {
const policies = [new Policy(policyData("1", "org1", PolicyType.MasterPassword, true))];
const result = policyService.combinePoliciesIntoMasterPasswordPolicyOptions(policies);
expect(result).toBeUndefined();
});
it("returns undefined when policies are not MasterPassword related", () => {
const unrelatedPolicyRequirements = {
minComplexity: 3,
minLength: 10,
requireUpper: true,
};
const policies = [
new Policy(
policyData(
"1",
"org1",
PolicyType.MaximumVaultTimeout,
true,
unrelatedPolicyRequirements,
),
),
new Policy(
policyData("2", "org2", PolicyType.DisableSend, true, unrelatedPolicyRequirements),
),
];
const result = policyService.combinePoliciesIntoMasterPasswordPolicyOptions(policies);
expect(result).toBeUndefined();
});
});
function policyData(
id: string,
organizationId: string,

View File

@@ -89,6 +89,8 @@ export class DefaultPolicyService implements PolicyService {
const policies$ = policies ? of(policies) : this.policies$(userId);
return policies$.pipe(
map((obsPolicies) => {
// TODO: replace with this.combinePoliciesIntoMasterPasswordPolicyOptions(obsPolicies)) once
// FeatureFlag.PM16117_ChangeExistingPasswordRefactor is removed.
let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined;
const filteredPolicies =
obsPolicies.filter((p) => p.type === PolicyType.MasterPassword) ?? [];
@@ -146,6 +148,47 @@ export class DefaultPolicyService implements PolicyService {
);
}
combinePoliciesIntoMasterPasswordPolicyOptions(
policies: Policy[],
): MasterPasswordPolicyOptions | undefined {
let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined;
const filteredPolicies = policies.filter((p) => p.type === PolicyType.MasterPassword) ?? [];
if (filteredPolicies.length === 0) {
return;
}
filteredPolicies.forEach((currentPolicy) => {
if (!currentPolicy.enabled || !currentPolicy.data) {
return undefined;
}
if (!enforcedOptions) {
enforcedOptions = new MasterPasswordPolicyOptions();
}
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,
@@ -245,4 +288,28 @@ export class DefaultPolicyService implements PolicyService {
return organization.canManagePolicies;
}
}
private mergeMasterPasswordPolicyOptions(
target: MasterPasswordPolicyOptions | undefined,
source: MasterPasswordPolicyOptions | undefined,
) {
if (!target) {
target = new MasterPasswordPolicyOptions();
}
// For complexity and minLength, take the highest value.
// For boolean settings, enable it if either policy has it enabled (OR).
if (source) {
target.minComplexity = Math.max(
target.minComplexity,
source.minComplexity ?? target.minComplexity,
);
target.minLength = Math.max(target.minLength, source.minLength ?? target.minLength);
target.requireUpper = Boolean(target.requireUpper || source.requireUpper);
target.requireLower = Boolean(target.requireLower || source.requireLower);
target.requireNumbers = Boolean(target.requireNumbers || source.requireNumbers);
target.requireSpecial = Boolean(target.requireSpecial || source.requireSpecial);
target.enforceOnLogin = Boolean(target.enforceOnLogin || source.enforceOnLogin);
}
}
}

View File

@@ -0,0 +1,26 @@
import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
export class DefaultOrganizationInviteService implements OrganizationInviteService {
/**
* No-op implementation.
*/
async getOrganizationInvite(): Promise<OrganizationInvite | null> {
return null;
}
/**
* No-op implementation.
* @param invite an organization invite
*/
async setOrganizationInvitation(invite: OrganizationInvite): Promise<void> {
return;
}
/**
* No-op implementation.
* */
async clearOrganizationInvitation(): Promise<void> {
return;
}
}

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 { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite";
export abstract class OrganizationInviteService {
/**
* Returns the currently stored organization invite
*/
abstract getOrganizationInvite: () => Promise<OrganizationInvite | null>;
/**
* Stores a new organization invite
* @param invite an organization invite
* @throws if the invite is nullish
*/
abstract setOrganizationInvitation: (invite: OrganizationInvite) => Promise<void>;
/**
* Clears the currently stored organization invite
*/
abstract clearOrganizationInvitation: () => Promise<void>;
}

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);
}
}