1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 21:33:27 +00:00

[PM-19976] Implement SDK generator engines (#15063)

* Hook up sdk engines to feature flag

---------

Co-authored-by:  Audrey  <ajensen@bitwarden.com>
This commit is contained in:
adudek-bw
2025-07-28 16:52:19 -04:00
committed by GitHub
parent fd727ffc39
commit 33ed9ac6ac
17 changed files with 107 additions and 469 deletions

View File

@@ -1,11 +1,14 @@
import { NgModule } from "@angular/core";
import { from, take } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -83,6 +86,7 @@ const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvider>("S
registry: ExtensionRegistry,
logger: LogService,
environment: PlatformUtilsService,
configService: ConfigService,
) => {
let log: LogProvider;
if (environment.isDev()) {
@@ -102,6 +106,7 @@ const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvider>("S
policy,
extension,
log,
configService,
};
},
deps: [
@@ -111,6 +116,7 @@ const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvider>("S
ExtensionRegistry,
LogService,
PlatformUtilsService,
ConfigService,
],
}),
safeProvider({
@@ -130,17 +136,26 @@ const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvider>("S
now: Date.now,
} satisfies UserStateSubjectDependencyProvider;
const featureFlagObs$ = from(
system.configService.getFeatureFlag(FeatureFlag.UseSdkPasswordGenerators),
);
let featureFlag: boolean = false;
featureFlagObs$.pipe(take(1)).subscribe((ff) => (featureFlag = ff));
const metadata = new providers.GeneratorMetadataProvider(
userStateDeps,
system,
Object.values(BuiltIn),
);
const sdkService = featureFlag ? system.sdk : undefined;
const profile = new providers.GeneratorProfileProvider(userStateDeps, system.policy);
const generator: providers.GeneratorDependencyProvider = {
randomizer: random,
client: new RestClient(api, i18n),
i18nService: i18n,
sdk: sdkService,
now: Date.now,
};
const userState: UserStateSubjectDependencyProvider = {

View File

@@ -30,7 +30,7 @@ describe("PasswordRandomizer", () => {
describe("randomAscii", () => {
it("returns the empty string when no character sets are specified", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomAscii({
all: 1,
@@ -41,7 +41,7 @@ describe("PasswordRandomizer", () => {
});
it("generates an uppercase ascii password", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomAscii({
all: 0,
@@ -54,7 +54,7 @@ describe("PasswordRandomizer", () => {
});
it("generates an uppercase ascii password without ambiguous characters", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomAscii({
all: 0,
@@ -67,7 +67,7 @@ describe("PasswordRandomizer", () => {
});
it("generates a lowercase ascii password", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomAscii({
all: 0,
@@ -80,7 +80,7 @@ describe("PasswordRandomizer", () => {
});
it("generates a lowercase ascii password without ambiguous characters", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomAscii({
all: 0,
@@ -93,7 +93,7 @@ describe("PasswordRandomizer", () => {
});
it("generates a numeric ascii password", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomAscii({
all: 0,
@@ -106,7 +106,7 @@ describe("PasswordRandomizer", () => {
});
it("generates a numeric password without ambiguous characters", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomAscii({
all: 0,
@@ -119,7 +119,7 @@ describe("PasswordRandomizer", () => {
});
it("generates a special character password", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomAscii({
all: 0,
@@ -132,7 +132,7 @@ describe("PasswordRandomizer", () => {
});
it("generates a special character password without ambiguous characters", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomAscii({
all: 0,
@@ -148,7 +148,7 @@ describe("PasswordRandomizer", () => {
[2, "AA"],
[3, "AAA"],
])("includes %p uppercase characters", async (uppercase, expected) => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomAscii({
all: 0,
@@ -163,7 +163,7 @@ describe("PasswordRandomizer", () => {
[2, "aa"],
[3, "aaa"],
])("includes %p lowercase characters", async (lowercase, expected) => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomAscii({
all: 0,
@@ -178,7 +178,7 @@ describe("PasswordRandomizer", () => {
[2, "00"],
[3, "000"],
])("includes %p digits", async (digits, expected) => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomAscii({
all: 0,
@@ -193,7 +193,7 @@ describe("PasswordRandomizer", () => {
[2, "!!"],
[3, "!!!"],
])("includes %p special characters", async (special, expected) => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomAscii({
all: 0,
@@ -212,7 +212,7 @@ describe("PasswordRandomizer", () => {
])(
"mixes character sets for the remaining characters (=%p)",
async (setting: Partial<RandomAsciiRequest>, set: CharacterSet) => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
await password.randomAscii({
...setting,
@@ -225,7 +225,7 @@ describe("PasswordRandomizer", () => {
);
it("shuffles the password characters", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
// Typically `shuffle` randomizes the order of the array it's been
// given. In the password generator, the array is generated from the
@@ -247,7 +247,7 @@ describe("PasswordRandomizer", () => {
describe("randomEffLongWords", () => {
it("generates the empty string when no words are passed", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomEffLongWords({
numberOfWords: 0,
@@ -263,7 +263,7 @@ describe("PasswordRandomizer", () => {
[1, "foo"],
[2, "foofoo"],
])("generates a %i-length word list", async (words, expected) => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomEffLongWords({
numberOfWords: words,
@@ -280,7 +280,7 @@ describe("PasswordRandomizer", () => {
});
it("capitalizes the word list", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
randomizer.pickWord.mockResolvedValueOnce("Foo");
const result = await password.randomEffLongWords({
@@ -298,7 +298,7 @@ describe("PasswordRandomizer", () => {
});
it("includes a random number on a random word", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
randomizer.pickWord.mockResolvedValueOnce("foo");
randomizer.pickWord.mockResolvedValueOnce("foo1");
@@ -324,7 +324,7 @@ describe("PasswordRandomizer", () => {
});
it("includes a separator", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.randomEffLongWords({
numberOfWords: 2,
@@ -339,7 +339,7 @@ describe("PasswordRandomizer", () => {
describe("generate", () => {
it("processes password generation options", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.generate(
{ algorithm: Algorithm.password },
@@ -352,7 +352,7 @@ describe("PasswordRandomizer", () => {
});
it("processes passphrase generation options", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = await password.generate(
{ algorithm: Algorithm.passphrase },
@@ -365,7 +365,7 @@ describe("PasswordRandomizer", () => {
});
it("throws when it cannot recognize the options type", async () => {
const password = new PasswordRandomizer(randomizer);
const password = new PasswordRandomizer(randomizer, Date.now);
const result = password.generate({ algorithm: Algorithm.username }, {});

View File

@@ -25,7 +25,10 @@ export class PasswordRandomizer
/** Instantiates the password randomizer
* @param randomizer data source for random data
*/
constructor(private randomizer: Randomizer) {}
constructor(
private randomizer: Randomizer,
private currentTime: () => number,
) {}
/** create a password from ASCII codepoints
* @param request refines the generated password
@@ -88,7 +91,7 @@ export class PasswordRandomizer
return new GeneratedCredential(
password,
Type.password,
Date.now(),
this.currentTime(),
request.source,
request.website,
);
@@ -99,7 +102,7 @@ export class PasswordRandomizer
return new GeneratedCredential(
passphrase,
Type.password,
Date.now(),
this.currentTime(),
request.source,
request.website,
);

View File

@@ -8,12 +8,6 @@ export const Algorithm = Object.freeze({
/** A password composed of random words from the EFF word list */
passphrase: "passphrase",
/** A password composed of random characters, retrieved from SDK */
sdkPassword: "sdkPassword",
/** A password composed of random words from the EFF word list, retrieved from SDK */
sdkPassphrase: "sdkPassphrase",
/** A username composed of words from the EFF word list */
username: "username",
@@ -44,12 +38,7 @@ export const Profile = Object.freeze({
/** Credential generation algorithms grouped by purpose. */
export const AlgorithmsByType = deepFreeze({
/** Algorithms that produce passwords */
[Type.password]: [
Algorithm.password,
Algorithm.passphrase,
Algorithm.sdkPassword,
Algorithm.sdkPassphrase,
] as const,
[Type.password]: [Algorithm.password, Algorithm.passphrase] as const,
/** Algorithms that produce usernames */
[Type.username]: [Algorithm.username] as const,

View File

@@ -3,7 +3,7 @@ import { mock } from "jest-mock-extended";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { PasswordRandomizer } from "../../engine";
import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine";
import { PassphrasePolicyConstraints } from "../../policies";
import { GeneratorDependencyProvider } from "../../providers";
import { PassphraseGenerationOptions } from "../../types";
@@ -17,8 +17,18 @@ const dependencyProvider = mock<GeneratorDependencyProvider>();
describe("password - eff words generator metadata", () => {
describe("engine.create", () => {
it("returns an email randomizer", () => {
expect(effPassphrase.engine.create(dependencyProvider)).toBeInstanceOf(PasswordRandomizer);
it("returns an sdk password randomizer", () => {
expect(effPassphrase.engine.create(dependencyProvider)).toBeInstanceOf(SdkPasswordRandomizer);
});
});
describe("engine.create", () => {
const nonSdkDependencyProvider = mock<GeneratorDependencyProvider>();
nonSdkDependencyProvider.sdk = undefined;
it("returns a password randomizer", () => {
expect(effPassphrase.engine.create(nonSdkDependencyProvider)).toBeInstanceOf(
PasswordRandomizer,
);
});
});

View File

@@ -3,7 +3,7 @@ import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
import { PasswordRandomizer } from "../../engine";
import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine";
import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies";
import { GeneratorDependencyProvider } from "../../providers";
import { CredentialGenerator, PassphraseGenerationOptions } from "../../types";
@@ -30,7 +30,10 @@ const passphrase: GeneratorMetadata<PassphraseGenerationOptions> = {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<PassphraseGenerationOptions> {
return new PasswordRandomizer(dependencies.randomizer);
if (dependencies.sdk == undefined) {
return new PasswordRandomizer(dependencies.randomizer, dependencies.now);
}
return new SdkPasswordRandomizer(dependencies.sdk, dependencies.now);
},
},
profiles: {

View File

@@ -3,7 +3,7 @@ import { mock } from "jest-mock-extended";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { PasswordRandomizer } from "../../engine";
import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine";
import { DynamicPasswordPolicyConstraints } from "../../policies";
import { GeneratorDependencyProvider } from "../../providers";
import { PasswordGenerationOptions } from "../../types";
@@ -17,8 +17,16 @@ const dependencyProvider = mock<GeneratorDependencyProvider>();
describe("password - characters generator metadata", () => {
describe("engine.create", () => {
it("returns an email randomizer", () => {
expect(password.engine.create(dependencyProvider)).toBeInstanceOf(PasswordRandomizer);
it("returns an sdk password randomizer", () => {
expect(password.engine.create(dependencyProvider)).toBeInstanceOf(SdkPasswordRandomizer);
});
});
describe("engine.create", () => {
const nonSdkDependencyProvider = mock<GeneratorDependencyProvider>();
nonSdkDependencyProvider.sdk = undefined;
it("returns a password randomizer", () => {
expect(password.engine.create(nonSdkDependencyProvider)).toBeInstanceOf(PasswordRandomizer);
});
});

View File

@@ -3,7 +3,7 @@ import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
import { deepFreeze } from "@bitwarden/common/tools/util";
import { PasswordRandomizer } from "../../engine";
import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine";
import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies";
import { GeneratorDependencyProvider } from "../../providers";
import { CredentialGenerator, PasswordGeneratorSettings } from "../../types";
@@ -30,7 +30,10 @@ const password: GeneratorMetadata<PasswordGeneratorSettings> = deepFreeze({
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<PasswordGeneratorSettings> {
return new PasswordRandomizer(dependencies.randomizer);
if (dependencies.sdk == undefined) {
return new PasswordRandomizer(dependencies.randomizer, dependencies.now);
}
return new SdkPasswordRandomizer(dependencies.sdk, dependencies.now);
},
},
profiles: {

View File

@@ -1,107 +0,0 @@
import { mock } from "jest-mock-extended";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { SdkPasswordRandomizer } from "../../engine";
import { PassphrasePolicyConstraints } from "../../policies";
import { GeneratorDependencyProvider } from "../../providers";
import { PassphraseGenerationOptions } from "../../types";
import { Profile } from "../data";
import { CoreProfileMetadata } from "../profile-metadata";
import { isCoreProfile } from "../util";
import sdkEffPassphrase from "./sdk-eff-word-list";
const dependencyProvider = mock<GeneratorDependencyProvider>();
describe("password - eff words generator metadata", () => {
describe("engine.create", () => {
it("returns an email randomizer", () => {
expect(sdkEffPassphrase.engine.create(dependencyProvider)).toBeInstanceOf(
SdkPasswordRandomizer,
);
});
});
describe("profiles[account]", () => {
let accountProfile: CoreProfileMetadata<PassphraseGenerationOptions> | null = null;
beforeEach(() => {
const profile = sdkEffPassphrase.profiles[Profile.account];
if (isCoreProfile(profile!)) {
accountProfile = profile;
} else {
accountProfile = null;
}
});
describe("storage.options.deserializer", () => {
it("returns its input", () => {
const value: PassphraseGenerationOptions = { ...accountProfile!.storage.initial };
const result = accountProfile!.storage.options.deserializer(value);
expect(result).toBe(value);
});
});
describe("constraints.create", () => {
// these tests check that the wiring is correct by exercising the behavior
// of functionality encapsulated by `create`. These methods may fail if the
// enclosed behaviors change.
it("creates a passphrase policy constraints", () => {
const context = { defaultConstraints: accountProfile!.constraints.default };
const constraints = accountProfile!.constraints.create([], context);
expect(constraints).toBeInstanceOf(PassphrasePolicyConstraints);
});
it("forwards the policy to the constraints", () => {
const context = { defaultConstraints: accountProfile!.constraints.default };
const policies = [
{
type: PolicyType.PasswordGenerator,
data: {
minNumberWords: 6,
capitalize: false,
includeNumber: false,
},
},
] as Policy[];
const constraints = accountProfile!.constraints.create(policies, context);
expect(constraints.constraints.numWords?.min).toEqual(6);
});
it("combines multiple policies in the constraints", () => {
const context = { defaultConstraints: accountProfile!.constraints.default };
const policies = [
{
type: PolicyType.PasswordGenerator,
data: {
minNumberWords: 6,
capitalize: false,
includeNumber: false,
},
},
{
type: PolicyType.PasswordGenerator,
data: {
minNumberWords: 3,
capitalize: true,
includeNumber: false,
},
},
] as Policy[];
const constraints = accountProfile!.constraints.create(policies, context);
expect(constraints.constraints.numWords?.min).toEqual(6);
expect(constraints.constraints.capitalize?.requiredValue).toEqual(true);
});
});
});
});

View File

@@ -1,89 +0,0 @@
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
import { BitwardenClient } from "@bitwarden/sdk-internal";
import { SdkPasswordRandomizer } from "../../engine";
import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies";
import { GeneratorDependencyProvider } from "../../providers";
import { CredentialGenerator, PassphraseGenerationOptions } from "../../types";
import { Algorithm, Profile, Type } from "../data";
import { GeneratorMetadata } from "../generator-metadata";
const sdkPassphrase: GeneratorMetadata<PassphraseGenerationOptions> = {
id: Algorithm.sdkPassphrase,
type: Type.password,
weight: 130,
i18nKeys: {
name: "passphrase",
credentialType: "passphrase",
generateCredential: "generatePassphrase",
credentialGenerated: "passphraseGenerated",
copyCredential: "copyPassphrase",
useCredential: "useThisPassphrase",
},
capabilities: {
autogenerate: false,
fields: [],
},
engine: {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<PassphraseGenerationOptions> {
return new SdkPasswordRandomizer(new BitwardenClient(null), Date.now); // @TODO hook up a real SDK client
},
},
profiles: {
[Profile.account]: {
type: "core",
storage: {
key: "passphraseGeneratorSettings",
target: "object",
format: "plain",
classifier: new PublicClassifier<PassphraseGenerationOptions>([
"numWords",
"wordSeparator",
"capitalize",
"includeNumber",
]),
state: GENERATOR_DISK,
initial: {
numWords: 6,
wordSeparator: "-",
capitalize: false,
includeNumber: false,
},
options: {
deserializer(value) {
return value;
},
clearOn: ["logout"],
},
} satisfies ObjectKey<PassphraseGenerationOptions>,
constraints: {
type: PolicyType.PasswordGenerator,
default: {
wordSeparator: { maxLength: 1 },
numWords: {
min: 3,
max: 20,
recommendation: 6,
},
},
create(policies, context) {
const initial = {
minNumberWords: 0,
capitalize: false,
includeNumber: false,
};
const policy = policies.reduce(passphraseLeastPrivilege, initial);
const constraints = new PassphrasePolicyConstraints(policy, context.defaultConstraints);
return constraints;
},
},
},
},
};
export default sdkPassphrase;

View File

@@ -1,108 +0,0 @@
import { mock } from "jest-mock-extended";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { SdkPasswordRandomizer } from "../../engine";
import { DynamicPasswordPolicyConstraints } from "../../policies";
import { GeneratorDependencyProvider } from "../../providers";
import { PasswordGenerationOptions } from "../../types";
import { Profile } from "../data";
import { CoreProfileMetadata } from "../profile-metadata";
import { isCoreProfile } from "../util";
import sdkPassword from "./sdk-random-password";
const dependencyProvider = mock<GeneratorDependencyProvider>();
describe("password - characters generator metadata", () => {
describe("engine.create", () => {
it("returns an email randomizer", () => {
expect(sdkPassword.engine.create(dependencyProvider)).toBeInstanceOf(SdkPasswordRandomizer);
});
});
describe("profiles[account]", () => {
let accountProfile: CoreProfileMetadata<PasswordGenerationOptions> = null!;
beforeEach(() => {
const profile = sdkPassword.profiles[Profile.account];
if (isCoreProfile(profile!)) {
accountProfile = profile;
} else {
throw new Error("this branch should never run");
}
});
describe("storage.options.deserializer", () => {
it("returns its input", () => {
const value: PasswordGenerationOptions = { ...accountProfile.storage.initial };
const result = accountProfile.storage.options.deserializer(value);
expect(result).toBe(value);
});
});
describe("constraints.create", () => {
// these tests check that the wiring is correct by exercising the behavior
// of functionality encapsulated by `create`. These methods may fail if the
// enclosed behaviors change.
it("creates a passphrase policy constraints", () => {
const context = { defaultConstraints: accountProfile.constraints.default };
const constraints = accountProfile.constraints.create([], context);
expect(constraints).toBeInstanceOf(DynamicPasswordPolicyConstraints);
});
it("forwards the policy to the constraints", () => {
const context = { defaultConstraints: accountProfile.constraints.default };
const policies = [
{
type: PolicyType.PasswordGenerator,
enabled: true,
data: {
minLength: 10,
capitalize: false,
useNumbers: false,
},
},
] as Policy[];
const constraints = accountProfile.constraints.create(policies, context);
expect(constraints.constraints.length?.min).toEqual(10);
});
it("combines multiple policies in the constraints", () => {
const context = { defaultConstraints: accountProfile.constraints.default };
const policies = [
{
type: PolicyType.PasswordGenerator,
enabled: true,
data: {
minLength: 14,
useSpecial: false,
useNumbers: false,
},
},
{
type: PolicyType.PasswordGenerator,
enabled: true,
data: {
minLength: 10,
useSpecial: true,
includeNumber: false,
},
},
] as Policy[];
const constraints = accountProfile.constraints.create(policies, context);
expect(constraints.constraints.length?.min).toEqual(14);
expect(constraints.constraints.special?.requiredValue).toEqual(true);
});
});
});
});

View File

@@ -1,115 +0,0 @@
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
import { deepFreeze } from "@bitwarden/common/tools/util";
import { BitwardenClient } from "@bitwarden/sdk-internal";
import { SdkPasswordRandomizer } from "../../engine";
import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies";
import { GeneratorDependencyProvider } from "../../providers";
import { CredentialGenerator, PasswordGeneratorSettings } from "../../types";
import { Algorithm, Profile, Type } from "../data";
import { GeneratorMetadata } from "../generator-metadata";
const sdkPassword: GeneratorMetadata<PasswordGeneratorSettings> = deepFreeze({
id: Algorithm.sdkPassword,
type: Type.password,
weight: 120,
i18nKeys: {
name: "password",
generateCredential: "generatePassword",
credentialGenerated: "passwordGenerated",
credentialType: "password",
copyCredential: "copyPassword",
useCredential: "useThisPassword",
},
capabilities: {
autogenerate: true,
fields: [],
},
engine: {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<PasswordGeneratorSettings> {
return new SdkPasswordRandomizer(new BitwardenClient(null), Date.now); // @TODO hook up a real SDK client
},
},
profiles: {
[Profile.account]: {
type: "core",
storage: {
key: "passwordGeneratorSettings",
target: "object",
format: "plain",
classifier: new PublicClassifier<PasswordGeneratorSettings>([
"length",
"ambiguous",
"uppercase",
"minUppercase",
"lowercase",
"minLowercase",
"number",
"minNumber",
"special",
"minSpecial",
]),
state: GENERATOR_DISK,
initial: {
length: 14,
ambiguous: true,
uppercase: true,
minUppercase: 1,
lowercase: true,
minLowercase: 1,
number: true,
minNumber: 1,
special: false,
minSpecial: 0,
},
options: {
deserializer(value) {
return value;
},
clearOn: ["logout"],
},
},
constraints: {
type: PolicyType.PasswordGenerator,
default: {
length: {
min: 5,
max: 128,
recommendation: 14,
},
minNumber: {
min: 0,
max: 9,
},
minSpecial: {
min: 0,
max: 9,
},
},
create(policies, context) {
const initial = {
minLength: 0,
useUppercase: false,
useLowercase: false,
useNumbers: false,
numberCount: 0,
useSpecial: false,
specialCount: 0,
};
const policy = policies.reduce(passwordLeastPrivilege, initial);
const constraints = new DynamicPasswordPolicyConstraints(
policy,
context.defaultConstraints,
);
return constraints;
},
},
},
},
});
export default sdkPassword;

View File

@@ -1,5 +1,6 @@
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
import { BitwardenClient } from "@bitwarden/sdk-internal";
import { Randomizer } from "../abstractions";
@@ -9,4 +10,6 @@ export type GeneratorDependencyProvider = {
// FIXME: introduce `I18nKeyOrLiteral` into forwarder
// structures and remove this dependency
i18nService: I18nService;
sdk?: BitwardenClient;
now: () => number;
};

View File

@@ -5,6 +5,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
import { UserEncryptor } from "@bitwarden/common/tools/cryptography/user-encryptor.abstraction";
import {
@@ -22,6 +23,7 @@ import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subje
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
import { deepFreeze } from "@bitwarden/common/tools/util";
import { UserId } from "@bitwarden/common/types/guid";
import { BitwardenClient } from "@bitwarden/sdk-internal";
import { FakeAccountService, FakeStateProvider } from "../../../../../common/spec";
import { Algorithm, AlgorithmsByType, CredentialAlgorithm, Type, Types } from "../metadata";
@@ -89,6 +91,10 @@ const SomePolicyService = mock<PolicyService>();
const SomeExtensionService = mock<ExtensionService>();
const SomeConfigService = mock<ConfigService>;
const SomeSdkService = mock<BitwardenClient>;
const ApplicationProvider = {
/** Policy configured by the administrative console */
policy: SomePolicyService,
@@ -98,7 +104,13 @@ const ApplicationProvider = {
/** Event monitoring and diagnostic interfaces */
log: disabledSemanticLoggerProvider,
} as SystemServiceProvider;
/** Feature flag retrieval */
configService: SomeConfigService,
/** SDK access for password generation */
sdk: SomeSdkService,
} as unknown as SystemServiceProvider;
describe("GeneratorMetadataProvider", () => {
beforeEach(() => {

View File

@@ -25,7 +25,7 @@ export function legacyPasswordGenerationServiceFactory(
stateProvider: StateProvider,
): PasswordGenerationServiceAbstraction {
const randomizer = new KeyServiceRandomizer(keyService);
const passwordRandomizer = new PasswordRandomizer(randomizer);
const passwordRandomizer = new PasswordRandomizer(randomizer, Date.now);
const passwords = new DefaultGeneratorService(
new PasswordGeneratorStrategy(passwordRandomizer, stateProvider),