mirror of
https://github.com/bitwarden/browser
synced 2026-02-16 00:24:52 +00:00
Merge branch 'main' into auth/pm-9115/implement-view-data-persistence-in-2FA-flows
This commit is contained in:
21
.github/workflows/test.yml
vendored
21
.github/workflows/test.yml
vendored
@@ -11,28 +11,10 @@ on:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
check-test-secrets:
|
||||
name: Check for test secrets
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
available: ${{ steps.check-test-secrets.outputs.available }}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Check
|
||||
id: check-test-secrets
|
||||
run: |
|
||||
if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then
|
||||
echo "available=true" >> $GITHUB_OUTPUT;
|
||||
else
|
||||
echo "available=false" >> $GITHUB_OUTPUT;
|
||||
fi
|
||||
|
||||
testing:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-test-secrets
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
@@ -77,7 +59,7 @@ jobs:
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
|
||||
if: ${{ needs.check-test-secrets.outputs.available == 'true' && !cancelled() }}
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
path: "junit.xml"
|
||||
@@ -89,7 +71,6 @@ jobs:
|
||||
|
||||
- name: Upload results to codecov.io
|
||||
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
|
||||
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
slot="header"
|
||||
[backAction]="close"
|
||||
showBackButton
|
||||
[pageTitle]="title"
|
||||
[pageTitle]="titleKey | i18n"
|
||||
></popup-header>
|
||||
|
||||
<vault-cipher-form-generator
|
||||
[type]="params.type"
|
||||
[uri]="uri"
|
||||
(valueGenerated)="onValueGenerated($event)"
|
||||
(algorithmSelected)="onAlgorithmSelected($event)"
|
||||
></vault-cipher-form-generator>
|
||||
|
||||
<popup-footer slot="footer">
|
||||
@@ -19,6 +20,7 @@
|
||||
buttonType="primary"
|
||||
(click)="selectValue()"
|
||||
data-testid="select-button"
|
||||
[disabled]="!(selectButtonText && generatedValue)"
|
||||
>
|
||||
{{ selectButtonText }}
|
||||
</button>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { AlgorithmInfo } from "@bitwarden/generator-core";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
|
||||
|
||||
import {
|
||||
GeneratorDialogAction,
|
||||
GeneratorDialogParams,
|
||||
GeneratorDialogResult,
|
||||
VaultGeneratorDialogComponent,
|
||||
@@ -21,8 +24,9 @@ import {
|
||||
standalone: true,
|
||||
})
|
||||
class MockCipherFormGenerator {
|
||||
@Input() type: "password" | "username";
|
||||
@Input() uri: string;
|
||||
@Input() type: "password" | "username" = "password";
|
||||
@Output() algorithmSelected: EventEmitter<AlgorithmInfo> = new EventEmitter();
|
||||
@Input() uri: string = "";
|
||||
@Output() valueGenerated = new EventEmitter<string>();
|
||||
}
|
||||
|
||||
@@ -53,34 +57,87 @@ describe("VaultGeneratorDialogComponent", () => {
|
||||
|
||||
fixture = TestBed.createComponent(VaultGeneratorDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
fixture.detectChanges();
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should use the appropriate text based on generator type", () => {
|
||||
expect(component["title"]).toBe("passwordGenerator");
|
||||
expect(component["selectButtonText"]).toBe("useThisPassword");
|
||||
|
||||
dialogData.type = "username";
|
||||
|
||||
fixture = TestBed.createComponent(VaultGeneratorDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
expect(component["title"]).toBe("usernameGenerator");
|
||||
expect(component["selectButtonText"]).toBe("useThisUsername");
|
||||
it("should show password generator title", () => {
|
||||
const header = fixture.debugElement.query(By.css("popup-header")).componentInstance;
|
||||
expect(header.pageTitle).toBe("passwordGenerator");
|
||||
});
|
||||
|
||||
it("should close the dialog with the generated value when the user selects it", () => {
|
||||
component["generatedValue"] = "generated-value";
|
||||
it("should pass type to cipher form generator", () => {
|
||||
const generator = fixture.debugElement.query(
|
||||
By.css("vault-cipher-form-generator"),
|
||||
).componentInstance;
|
||||
expect(generator.type).toBe("password");
|
||||
});
|
||||
|
||||
fixture.nativeElement.querySelector("button[data-testid='select-button']").click();
|
||||
it("should enable select button when value is generated", () => {
|
||||
component.onAlgorithmSelected({ useGeneratedValue: "Test" } as any);
|
||||
component.onValueGenerated("test-password");
|
||||
fixture.detectChanges();
|
||||
|
||||
const button = fixture.debugElement.query(
|
||||
By.css("[data-testid='select-button']"),
|
||||
).nativeElement;
|
||||
expect(button.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("should disable the button if no value has been generated", () => {
|
||||
const generator = fixture.debugElement.query(
|
||||
By.css("vault-cipher-form-generator"),
|
||||
).componentInstance;
|
||||
|
||||
generator.algorithmSelected.emit({ useGeneratedValue: "Use Password" } as any);
|
||||
fixture.detectChanges();
|
||||
|
||||
const button = fixture.debugElement.query(
|
||||
By.css("[data-testid='select-button']"),
|
||||
).nativeElement;
|
||||
expect(button.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should disable the button if no algorithm is selected", () => {
|
||||
const generator = fixture.debugElement.query(
|
||||
By.css("vault-cipher-form-generator"),
|
||||
).componentInstance;
|
||||
|
||||
generator.valueGenerated.emit("test-password");
|
||||
fixture.detectChanges();
|
||||
|
||||
const button = fixture.debugElement.query(
|
||||
By.css("[data-testid='select-button']"),
|
||||
).nativeElement;
|
||||
expect(button.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should update button text when algorithm is selected", () => {
|
||||
component.onAlgorithmSelected({ useGeneratedValue: "Use This Password" } as any);
|
||||
fixture.detectChanges();
|
||||
|
||||
const button = fixture.debugElement.query(
|
||||
By.css("[data-testid='select-button']"),
|
||||
).nativeElement;
|
||||
expect(button.textContent.trim()).toBe("Use This Password");
|
||||
});
|
||||
|
||||
it("should close with generated value when selected", () => {
|
||||
component.onAlgorithmSelected({ useGeneratedValue: "Test" } as any);
|
||||
component.onValueGenerated("test-password");
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.debugElement.query(By.css("[data-testid='select-button']")).nativeElement.click();
|
||||
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
action: "selected",
|
||||
generatedValue: "generated-value",
|
||||
action: GeneratorDialogAction.Selected,
|
||||
generatedValue: "test-password",
|
||||
});
|
||||
});
|
||||
|
||||
it("should close with canceled action when dismissed", () => {
|
||||
fixture.debugElement.query(By.css("popup-header")).componentInstance.backAction();
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
action: GeneratorDialogAction.Canceled,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ButtonModule, DialogService } from "@bitwarden/components";
|
||||
import { AlgorithmInfo } from "@bitwarden/generator-core";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
|
||||
@@ -39,13 +41,12 @@ export enum GeneratorDialogAction {
|
||||
CommonModule,
|
||||
CipherFormGeneratorComponent,
|
||||
ButtonModule,
|
||||
I18nPipe,
|
||||
],
|
||||
})
|
||||
export class VaultGeneratorDialogComponent {
|
||||
protected title = this.i18nService.t(this.isPassword ? "passwordGenerator" : "usernameGenerator");
|
||||
protected selectButtonText = this.i18nService.t(
|
||||
this.isPassword ? "useThisPassword" : "useThisUsername",
|
||||
);
|
||||
protected selectButtonText: string | undefined;
|
||||
protected titleKey = this.isPassword ? "passwordGenerator" : "usernameGenerator";
|
||||
|
||||
/**
|
||||
* Whether the dialog is generating a password/passphrase. If false, it is generating a username.
|
||||
@@ -92,6 +93,16 @@ export class VaultGeneratorDialogComponent {
|
||||
this.generatedValue = value;
|
||||
}
|
||||
|
||||
onAlgorithmSelected = (selected?: AlgorithmInfo) => {
|
||||
if (selected) {
|
||||
this.selectButtonText = selected.useGeneratedValue;
|
||||
} else {
|
||||
// default to email
|
||||
this.selectButtonText = this.i18nService.t("useThisEmail");
|
||||
}
|
||||
this.generatedValue = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the vault generator dialog in a full screen dialog.
|
||||
*/
|
||||
|
||||
@@ -28,7 +28,7 @@ impl BitwardenDesktopAgent {
|
||||
show_ui_request_tx: auth_request_tx,
|
||||
get_ui_response_rx: auth_response_rx,
|
||||
request_id: Arc::new(AtomicU32::new(0)),
|
||||
needs_unlock: Arc::new(AtomicBool::new(false)),
|
||||
needs_unlock: Arc::new(AtomicBool::new(true)),
|
||||
is_running: Arc::new(AtomicBool::new(false)),
|
||||
};
|
||||
let cloned_agent_state = agent.clone();
|
||||
|
||||
@@ -130,7 +130,7 @@ export class SshAgentService implements OnDestroy {
|
||||
|
||||
throw error;
|
||||
}),
|
||||
map(() => message),
|
||||
map(() => [message, account.id]),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<vault-cipher-form-generator
|
||||
[type]="data.type"
|
||||
(valueGenerated)="onCredentialGenerated($event)"
|
||||
[onAlgorithmSelected]="onAlgorithmSelected"
|
||||
(algorithmSelected)="onAlgorithmSelected($event)"
|
||||
/>
|
||||
<bit-item>
|
||||
<button
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
@@ -44,16 +45,17 @@ export class CredentialGeneratorDialogComponent {
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: CredentialGeneratorParams,
|
||||
private dialogService: DialogService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
onAlgorithmSelected = (selected?: AlgorithmInfo) => {
|
||||
if (selected) {
|
||||
this.buttonLabel = selected.useGeneratedValue;
|
||||
} else {
|
||||
// clear the credential value when the user is
|
||||
// selecting the credential generation algorithm
|
||||
this.credentialValue = undefined;
|
||||
// default to email
|
||||
this.buttonLabel = this.i18nService.t("useThisEmail");
|
||||
}
|
||||
this.credentialValue = undefined;
|
||||
};
|
||||
|
||||
applyCredentials = () => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<bit-dialog dialogSize="default" background="alt">
|
||||
<span bitDialogTitle>
|
||||
{{ title }}
|
||||
{{ titleKey | i18n }}
|
||||
</span>
|
||||
<ng-container bitDialogContent>
|
||||
<vault-cipher-form-generator
|
||||
[type]="params.type"
|
||||
[uri]="uri"
|
||||
(valueGenerated)="onValueGenerated($event)"
|
||||
(algorithmSelected)="onAlgorithmSelected($event)"
|
||||
disableMargin
|
||||
></vault-cipher-form-generator>
|
||||
</ng-container>
|
||||
@@ -17,8 +18,9 @@
|
||||
buttonType="primary"
|
||||
(click)="selectValue()"
|
||||
data-testid="select-button"
|
||||
[disabled]="!(buttonLabel && generatedValue)"
|
||||
>
|
||||
{{ selectButtonText }}
|
||||
{{ buttonLabel }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Input, Output, EventEmitter } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { AlgorithmInfo } from "@bitwarden/generator-core";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
import {
|
||||
WebVaultGeneratorDialogAction,
|
||||
WebVaultGeneratorDialogComponent,
|
||||
WebVaultGeneratorDialogParams,
|
||||
WebVaultGeneratorDialogResult,
|
||||
} from "./web-generator-dialog.component";
|
||||
|
||||
@Component({
|
||||
@@ -22,7 +22,8 @@ import {
|
||||
standalone: true,
|
||||
})
|
||||
class MockCipherFormGenerator {
|
||||
@Input() type: "password" | "username";
|
||||
@Input() type: "password" | "username" = "password";
|
||||
@Output() algorithmSelected: EventEmitter<AlgorithmInfo> = new EventEmitter();
|
||||
@Input() uri?: string;
|
||||
@Output() valueGenerated = new EventEmitter<string>();
|
||||
}
|
||||
@@ -30,35 +31,20 @@ class MockCipherFormGenerator {
|
||||
describe("WebVaultGeneratorDialogComponent", () => {
|
||||
let component: WebVaultGeneratorDialogComponent;
|
||||
let fixture: ComponentFixture<WebVaultGeneratorDialogComponent>;
|
||||
|
||||
let dialogRef: MockProxy<DialogRef<any>>;
|
||||
let dialogRef: MockProxy<DialogRef<WebVaultGeneratorDialogResult>>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
dialogRef = mock<DialogRef<any>>();
|
||||
dialogRef = mock<DialogRef<WebVaultGeneratorDialogResult>>();
|
||||
mockI18nService = mock<I18nService>();
|
||||
|
||||
const mockDialogData: WebVaultGeneratorDialogParams = { type: "password" };
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, WebVaultGeneratorDialogComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: DialogRef,
|
||||
useValue: dialogRef,
|
||||
},
|
||||
{
|
||||
provide: DIALOG_DATA,
|
||||
useValue: mockDialogData,
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mockI18nService,
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: mock<PlatformUtilsService>(),
|
||||
},
|
||||
{ provide: DialogRef, useValue: dialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: { type: "password" } },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
],
|
||||
})
|
||||
.overrideComponent(WebVaultGeneratorDialogComponent, {
|
||||
@@ -72,38 +58,73 @@ describe("WebVaultGeneratorDialogComponent", () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("initializes without errors", () => {
|
||||
fixture.detectChanges();
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("closes the dialog with 'canceled' result when close is called", () => {
|
||||
const closeSpy = jest.spyOn(dialogRef, "close");
|
||||
it("should enable button when value and algorithm are selected", () => {
|
||||
const generator = fixture.debugElement.query(
|
||||
By.css("vault-cipher-form-generator"),
|
||||
).componentInstance;
|
||||
|
||||
(component as any).close();
|
||||
generator.algorithmSelected.emit({ useGeneratedValue: "Use Password" } as any);
|
||||
generator.valueGenerated.emit("test-password");
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(closeSpy).toHaveBeenCalledWith({
|
||||
const button = fixture.debugElement.query(
|
||||
By.css("[data-testid='select-button']"),
|
||||
).nativeElement;
|
||||
expect(button.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("should disable the button if no value has been generated", () => {
|
||||
const generator = fixture.debugElement.query(
|
||||
By.css("vault-cipher-form-generator"),
|
||||
).componentInstance;
|
||||
|
||||
generator.algorithmSelected.emit({ useGeneratedValue: "Use Password" } as any);
|
||||
fixture.detectChanges();
|
||||
|
||||
const button = fixture.debugElement.query(
|
||||
By.css("[data-testid='select-button']"),
|
||||
).nativeElement;
|
||||
expect(button.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should disable the button if no algorithm is selected", () => {
|
||||
const generator = fixture.debugElement.query(
|
||||
By.css("vault-cipher-form-generator"),
|
||||
).componentInstance;
|
||||
|
||||
generator.valueGenerated.emit("test-password");
|
||||
fixture.detectChanges();
|
||||
|
||||
const button = fixture.debugElement.query(
|
||||
By.css("[data-testid='select-button']"),
|
||||
).nativeElement;
|
||||
expect(button.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should close with selected value when confirmed", () => {
|
||||
const generator = fixture.debugElement.query(
|
||||
By.css("vault-cipher-form-generator"),
|
||||
).componentInstance;
|
||||
generator.algorithmSelected.emit({ useGeneratedValue: "Use Password" } as any);
|
||||
generator.valueGenerated.emit("test-password");
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.debugElement.query(By.css("[data-testid='select-button']")).nativeElement.click();
|
||||
|
||||
expect(dialogRef.close).toHaveBeenCalledWith({
|
||||
action: WebVaultGeneratorDialogAction.Selected,
|
||||
generatedValue: "test-password",
|
||||
});
|
||||
});
|
||||
|
||||
it("should close with canceled action when dismissed", () => {
|
||||
component["close"]();
|
||||
expect(dialogRef.close).toHaveBeenCalledWith({
|
||||
action: WebVaultGeneratorDialogAction.Canceled,
|
||||
});
|
||||
});
|
||||
|
||||
it("closes the dialog with 'selected' result when selectValue is called", () => {
|
||||
const closeSpy = jest.spyOn(dialogRef, "close");
|
||||
const generatedValue = "generated-value";
|
||||
component.onValueGenerated(generatedValue);
|
||||
|
||||
(component as any).selectValue();
|
||||
|
||||
expect(closeSpy).toHaveBeenCalledWith({
|
||||
action: WebVaultGeneratorDialogAction.Selected,
|
||||
generatedValue: generatedValue,
|
||||
});
|
||||
});
|
||||
|
||||
it("updates generatedValue when onValueGenerated is called", () => {
|
||||
const generatedValue = "new-generated-value";
|
||||
component.onValueGenerated(generatedValue);
|
||||
|
||||
expect((component as any).generatedValue).toBe(generatedValue);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||
import { AlgorithmInfo } from "@bitwarden/generator-core";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
export interface WebVaultGeneratorDialogParams {
|
||||
@@ -27,13 +29,11 @@ export enum WebVaultGeneratorDialogAction {
|
||||
selector: "web-vault-generator-dialog",
|
||||
templateUrl: "./web-generator-dialog.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, CipherFormGeneratorComponent, ButtonModule, DialogModule],
|
||||
imports: [CommonModule, CipherFormGeneratorComponent, ButtonModule, DialogModule, I18nPipe],
|
||||
})
|
||||
export class WebVaultGeneratorDialogComponent {
|
||||
protected title = this.i18nService.t(this.isPassword ? "passwordGenerator" : "usernameGenerator");
|
||||
protected selectButtonText = this.i18nService.t(
|
||||
this.isPassword ? "useThisPassword" : "useThisUsername",
|
||||
);
|
||||
protected titleKey = this.isPassword ? "passwordGenerator" : "usernameGenerator";
|
||||
protected buttonLabel: string | undefined;
|
||||
|
||||
/**
|
||||
* Whether the dialog is generating a password/passphrase. If false, it is generating a username.
|
||||
@@ -80,6 +80,16 @@ export class WebVaultGeneratorDialogComponent {
|
||||
this.generatedValue = value;
|
||||
}
|
||||
|
||||
onAlgorithmSelected = (selected?: AlgorithmInfo) => {
|
||||
if (selected) {
|
||||
this.buttonLabel = selected.useGeneratedValue;
|
||||
} else {
|
||||
// default to email
|
||||
this.buttonLabel = this.i18nService.t("useThisEmail");
|
||||
}
|
||||
this.generatedValue = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the vault generator dialog.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "../types/guid";
|
||||
|
||||
/** error emitted when the `SingleUserDependency` changes Ids */
|
||||
export type UserChangedError = {
|
||||
|
||||
@@ -6,6 +6,9 @@ import { DefaultSemanticLogger } from "./default-semantic-logger";
|
||||
import { DisabledSemanticLogger } from "./disabled-semantic-logger";
|
||||
import { SemanticLogger } from "./semantic-logger.abstraction";
|
||||
|
||||
/** A type for injection of a log provider */
|
||||
export type LogProvider = <Context>(context: Jsonify<Context>) => SemanticLogger;
|
||||
|
||||
/** Instantiates a semantic logger that emits nothing when a message
|
||||
* is logged.
|
||||
* @param _context a static payload that is cloned when the logger
|
||||
@@ -25,8 +28,11 @@ export function disabledSemanticLoggerProvider<Context extends object>(
|
||||
* @param settings specializes how the semantic logger functions.
|
||||
* If this is omitted, the logger suppresses debug messages.
|
||||
*/
|
||||
export function consoleSemanticLoggerProvider(logger: LogService): SemanticLogger {
|
||||
return new DefaultSemanticLogger(logger, {});
|
||||
export function consoleSemanticLoggerProvider<Context extends object>(
|
||||
logger: LogService,
|
||||
context: Jsonify<Context>,
|
||||
): SemanticLogger {
|
||||
return new DefaultSemanticLogger(logger, context);
|
||||
}
|
||||
|
||||
/** Instantiates a semantic logger that emits logs to the console.
|
||||
@@ -42,7 +48,7 @@ export function ifEnabledSemanticLoggerProvider<Context extends object>(
|
||||
context: Jsonify<Context>,
|
||||
) {
|
||||
if (enable) {
|
||||
return new DefaultSemanticLogger(logger, context);
|
||||
return consoleSemanticLoggerProvider(logger, context);
|
||||
} else {
|
||||
return disabledSemanticLoggerProvider(context);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export abstract class UserStateSubjectDependencyProvider {
|
||||
/** Provides local object persistence */
|
||||
abstract state: StateProvider;
|
||||
|
||||
// FIXME: remove `log` and inject the system provider into the USS instead
|
||||
/** Provides semantic logging */
|
||||
abstract log: <Context extends object>(_context: Jsonify<Context>) => SemanticLogger;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ const DEFAULT_FRAME_SIZE = 32;
|
||||
export class UserStateSubject<
|
||||
State extends object,
|
||||
Secret = State,
|
||||
Disclosed = never,
|
||||
Disclosed = Record<string, never>,
|
||||
Dependencies = null,
|
||||
>
|
||||
extends Observable<State>
|
||||
@@ -243,7 +243,7 @@ export class UserStateSubject<
|
||||
// `init$` becomes the accumulator for `scan`
|
||||
init$.pipe(
|
||||
first(),
|
||||
map((init) => [init, null] as const),
|
||||
map((init) => [init, null] as [State, Dependencies]),
|
||||
),
|
||||
input$.pipe(
|
||||
map((constrained) => constrained.state),
|
||||
@@ -256,7 +256,7 @@ export class UserStateSubject<
|
||||
if (shouldUpdate) {
|
||||
// actual update
|
||||
const next = this.context.nextValue?.(prev, pending, dependencies) ?? pending;
|
||||
return [next, dependencies];
|
||||
return [next, dependencies] as const;
|
||||
} else {
|
||||
// false update
|
||||
this.log.debug("shouldUpdate prevented write");
|
||||
|
||||
10
libs/tools/generator/core/src/metadata/index.ts
Normal file
10
libs/tools/generator/core/src/metadata/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { AlgorithmsByType as ABT } from "./data";
|
||||
import { CredentialType, CredentialAlgorithm } from "./type";
|
||||
|
||||
export const AlgorithmsByType: Record<CredentialType, ReadonlyArray<CredentialAlgorithm>> = ABT;
|
||||
|
||||
export { Profile, Type } from "./data";
|
||||
export { GeneratorMetadata } from "./generator-metadata";
|
||||
export { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "./profile-metadata";
|
||||
export { GeneratorProfile, CredentialAlgorithm, CredentialType } from "./type";
|
||||
export { isForwarderProfile, isForwarderExtensionId } from "./util";
|
||||
@@ -0,0 +1,338 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, ReplaySubject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
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 { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
|
||||
import { UserEncryptor } from "@bitwarden/common/tools/cryptography/user-encryptor.abstraction";
|
||||
import { disabledSemanticLoggerProvider } from "@bitwarden/common/tools/log";
|
||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
||||
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
|
||||
import { StateConstraints } from "@bitwarden/common/tools/types";
|
||||
import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { FakeStateProvider, FakeAccountService, awaitAsync } from "../../../../../common/spec";
|
||||
import { CoreProfileMetadata, ProfileContext } from "../metadata/profile-metadata";
|
||||
import { GeneratorConstraints } from "../types";
|
||||
|
||||
import { GeneratorProfileProvider } from "./generator-profile-provider";
|
||||
|
||||
// arbitrary settings types
|
||||
type SomeSettings = { foo: string };
|
||||
|
||||
// fake user information
|
||||
const SomeUser = "SomeUser" as UserId;
|
||||
const AnotherUser = "SomeOtherUser" as UserId;
|
||||
const UnverifiedEmailUser = "UnverifiedEmailUser" as UserId;
|
||||
const accounts: Record<UserId, Account> = {
|
||||
[SomeUser]: {
|
||||
id: SomeUser,
|
||||
name: "some user",
|
||||
email: "some.user@example.com",
|
||||
emailVerified: true,
|
||||
},
|
||||
[AnotherUser]: {
|
||||
id: AnotherUser,
|
||||
name: "some other user",
|
||||
email: "some.other.user@example.com",
|
||||
emailVerified: true,
|
||||
},
|
||||
[UnverifiedEmailUser]: {
|
||||
id: UnverifiedEmailUser,
|
||||
name: "a user with an unverfied email",
|
||||
email: "unverified@example.com",
|
||||
emailVerified: false,
|
||||
},
|
||||
};
|
||||
const accountService = new FakeAccountService(accounts);
|
||||
|
||||
const policyService = mock<PolicyService>();
|
||||
const somePolicy = new Policy({
|
||||
data: { fooPolicy: true },
|
||||
type: PolicyType.PasswordGenerator,
|
||||
id: "" as PolicyId,
|
||||
organizationId: "" as OrganizationId,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const stateProvider = new FakeStateProvider(accountService);
|
||||
const encryptor = mock<UserEncryptor>();
|
||||
const encryptorProvider = mock<LegacyEncryptorProvider>();
|
||||
|
||||
const dependencyProvider: UserStateSubjectDependencyProvider = {
|
||||
encryptor: encryptorProvider,
|
||||
state: stateProvider,
|
||||
log: disabledSemanticLoggerProvider,
|
||||
};
|
||||
|
||||
// settings storage location
|
||||
const SettingsKey = new UserKeyDefinition<SomeSettings>(GENERATOR_DISK, "SomeSettings", {
|
||||
deserializer: (value) => value,
|
||||
clearOn: [],
|
||||
});
|
||||
|
||||
// fake the configuration
|
||||
const SomeProfile: CoreProfileMetadata<SomeSettings> = {
|
||||
type: "core",
|
||||
storage: {
|
||||
target: "object",
|
||||
key: "SomeSettings",
|
||||
state: GENERATOR_DISK,
|
||||
classifier: new PrivateClassifier(),
|
||||
format: "plain",
|
||||
options: {
|
||||
deserializer: (value) => value,
|
||||
clearOn: [],
|
||||
},
|
||||
initial: { foo: "initial" },
|
||||
},
|
||||
constraints: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
default: { foo: {} },
|
||||
create: jest.fn((policies, context) => {
|
||||
const combined = policies.reduce(
|
||||
(acc, policy) => ({ fooPolicy: acc.fooPolicy || policy.data.fooPolicy }),
|
||||
{ fooPolicy: false },
|
||||
);
|
||||
|
||||
if (combined.fooPolicy) {
|
||||
return {
|
||||
constraints: {
|
||||
policyInEffect: true,
|
||||
},
|
||||
calibrate(state: SomeSettings) {
|
||||
return {
|
||||
constraints: {},
|
||||
adjust(state: SomeSettings) {
|
||||
return { foo: `adjusted(${state.foo})` };
|
||||
},
|
||||
fix(state: SomeSettings) {
|
||||
return { foo: `fixed(${state.foo})` };
|
||||
},
|
||||
} satisfies StateConstraints<SomeSettings>;
|
||||
},
|
||||
} satisfies GeneratorConstraints<SomeSettings>;
|
||||
} else {
|
||||
return {
|
||||
constraints: {
|
||||
policyInEffect: false,
|
||||
},
|
||||
adjust(state: SomeSettings) {
|
||||
return state;
|
||||
},
|
||||
fix(state: SomeSettings) {
|
||||
return state;
|
||||
},
|
||||
} satisfies GeneratorConstraints<SomeSettings>;
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const NoPolicyProfile: CoreProfileMetadata<SomeSettings> = {
|
||||
type: "core",
|
||||
storage: {
|
||||
target: "object",
|
||||
key: "SomeSettings",
|
||||
state: GENERATOR_DISK,
|
||||
classifier: new PrivateClassifier(),
|
||||
format: "classified",
|
||||
options: {
|
||||
deserializer: (value) => value,
|
||||
clearOn: [],
|
||||
},
|
||||
initial: { foo: "initial" },
|
||||
},
|
||||
constraints: {
|
||||
default: { foo: {} },
|
||||
create: jest.fn((policies, context) => new IdentityConstraint()),
|
||||
},
|
||||
};
|
||||
|
||||
describe("GeneratorProfileProvider", () => {
|
||||
beforeEach(async () => {
|
||||
policyService.getAll$.mockImplementation(() => new BehaviorSubject([]).asObservable());
|
||||
const encryptor$ = new BehaviorSubject({ userId: SomeUser, encryptor });
|
||||
encryptorProvider.userEncryptor$.mockReturnValue(encryptor$);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("settings", () => {
|
||||
it("writes to the user's state", async () => {
|
||||
const account$ = new BehaviorSubject(accounts[SomeUser]).asObservable();
|
||||
const profileProvider = new GeneratorProfileProvider(dependencyProvider, policyService);
|
||||
const settings = profileProvider.settings(SomeProfile, { account$ });
|
||||
|
||||
settings.next({ foo: "next value" });
|
||||
await awaitAsync();
|
||||
const result = await firstValueFrom(stateProvider.getUserState$(SettingsKey, SomeUser));
|
||||
|
||||
expect(result).toEqual({ foo: "next value" });
|
||||
});
|
||||
|
||||
it("waits for the user to become available", async () => {
|
||||
await stateProvider.setUserState(SettingsKey, { foo: "initial value" }, SomeUser);
|
||||
const account = new ReplaySubject<Account>(1);
|
||||
const account$ = account.asObservable();
|
||||
const profileProvider = new GeneratorProfileProvider(dependencyProvider, policyService);
|
||||
|
||||
let result: SomeSettings | undefined = undefined;
|
||||
profileProvider.settings(SomeProfile, { account$ }).subscribe({
|
||||
next(settings) {
|
||||
result = settings;
|
||||
},
|
||||
});
|
||||
await awaitAsync();
|
||||
expect(result).toBeUndefined();
|
||||
account.next(accounts[SomeUser]);
|
||||
await awaitAsync();
|
||||
|
||||
// need to use `!` because TypeScript isn't aware that the subscription
|
||||
// sets `result`, and thus computes the type of `result?.userId` as `never`
|
||||
expect(result).toEqual({ foo: "initial value" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("constraints$", () => {
|
||||
it("creates constraints without policy in effect when there is no policy", async () => {
|
||||
const profileProvider = new GeneratorProfileProvider(dependencyProvider, policyService);
|
||||
const account$ = new BehaviorSubject(accounts[SomeUser]).asObservable();
|
||||
|
||||
const result = await firstValueFrom(profileProvider.constraints$(SomeProfile, { account$ }));
|
||||
|
||||
expect(result.constraints.policyInEffect).toBeFalsy();
|
||||
});
|
||||
|
||||
it("creates constraints with policy in effect when there is a policy", async () => {
|
||||
const profileProvider = new GeneratorProfileProvider(dependencyProvider, policyService);
|
||||
const account$ = new BehaviorSubject(accounts[SomeUser]).asObservable();
|
||||
const policy$ = new BehaviorSubject([somePolicy]);
|
||||
policyService.getAll$.mockReturnValue(policy$);
|
||||
|
||||
const result = await firstValueFrom(profileProvider.constraints$(SomeProfile, { account$ }));
|
||||
|
||||
expect(result.constraints.policyInEffect).toBeTruthy();
|
||||
});
|
||||
|
||||
it("sends the policy list to profile.constraint.create(...) when a type is specified", async () => {
|
||||
const profileProvider = new GeneratorProfileProvider(dependencyProvider, policyService);
|
||||
const account$ = new BehaviorSubject(accounts[SomeUser]).asObservable();
|
||||
const expectedPolicy = [somePolicy];
|
||||
const policy$ = new BehaviorSubject(expectedPolicy);
|
||||
policyService.getAll$.mockReturnValue(policy$);
|
||||
|
||||
await firstValueFrom(profileProvider.constraints$(SomeProfile, { account$ }));
|
||||
|
||||
expect(SomeProfile.constraints.create).toHaveBeenCalledWith(
|
||||
expectedPolicy,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("sends an empty policy list to profile.constraint.create(...) when a type is omitted", async () => {
|
||||
const profileProvider = new GeneratorProfileProvider(dependencyProvider, policyService);
|
||||
const account$ = new BehaviorSubject(accounts[SomeUser]).asObservable();
|
||||
|
||||
await firstValueFrom(profileProvider.constraints$(NoPolicyProfile, { account$ }));
|
||||
|
||||
expect(NoPolicyProfile.constraints.create).toHaveBeenCalledWith([], expect.any(Object));
|
||||
});
|
||||
|
||||
it("sends the context to profile.constraint.create(...)", async () => {
|
||||
const profileProvider = new GeneratorProfileProvider(dependencyProvider, policyService);
|
||||
const account$ = new BehaviorSubject(accounts[SomeUser]).asObservable();
|
||||
const expectedContext: ProfileContext<SomeSettings> = {
|
||||
defaultConstraints: NoPolicyProfile.constraints.default,
|
||||
email: accounts[SomeUser].email,
|
||||
};
|
||||
|
||||
await firstValueFrom(profileProvider.constraints$(NoPolicyProfile, { account$ }));
|
||||
|
||||
expect(NoPolicyProfile.constraints.create).toHaveBeenCalledWith(
|
||||
expect.any(Array),
|
||||
expectedContext,
|
||||
);
|
||||
});
|
||||
|
||||
it("omits nonverified emails from the context sent to profile.constraint.create(...)", async () => {
|
||||
const profileProvider = new GeneratorProfileProvider(dependencyProvider, policyService);
|
||||
const account$ = new BehaviorSubject(accounts[UnverifiedEmailUser]).asObservable();
|
||||
const expectedContext: ProfileContext<SomeSettings> = {
|
||||
defaultConstraints: NoPolicyProfile.constraints.default,
|
||||
};
|
||||
|
||||
await firstValueFrom(profileProvider.constraints$(NoPolicyProfile, { account$ }));
|
||||
|
||||
expect(NoPolicyProfile.constraints.create).toHaveBeenCalledWith(
|
||||
expect.any(Array),
|
||||
expectedContext,
|
||||
);
|
||||
});
|
||||
|
||||
// FIXME: implement this test case once the fake account service mock supports email verification
|
||||
it.todo("invokes profile.constraint.create(...) when the user's email address is verified");
|
||||
|
||||
// FIXME: implement this test case once the fake account service mock supports email updates
|
||||
it.todo("invokes profile.constraint.create(...) when the user's email address changes");
|
||||
|
||||
it("follows policy emissions", async () => {
|
||||
const profileProvider = new GeneratorProfileProvider(dependencyProvider, policyService);
|
||||
const account = new BehaviorSubject(accounts[SomeUser]);
|
||||
const account$ = account.asObservable();
|
||||
const somePolicySubject = new BehaviorSubject([somePolicy]);
|
||||
policyService.getAll$.mockReturnValueOnce(somePolicySubject.asObservable());
|
||||
const emissions: GeneratorConstraints<SomeSettings>[] = [];
|
||||
const sub = profileProvider
|
||||
.constraints$(SomeProfile, { account$ })
|
||||
.subscribe((policy) => emissions.push(policy));
|
||||
|
||||
// swap the active policy for an inactive policy
|
||||
somePolicySubject.next([]);
|
||||
await awaitAsync();
|
||||
sub.unsubscribe();
|
||||
const [someResult, anotherResult] = emissions;
|
||||
|
||||
expect(someResult.constraints.policyInEffect).toBeTruthy();
|
||||
expect(anotherResult.constraints.policyInEffect).toBeFalsy();
|
||||
});
|
||||
|
||||
it("errors when the user errors", async () => {
|
||||
const profileProvider = new GeneratorProfileProvider(dependencyProvider, policyService);
|
||||
const account = new BehaviorSubject(accounts[SomeUser]);
|
||||
const account$ = account.asObservable();
|
||||
const expectedError = { some: "error" };
|
||||
|
||||
let actualError: any = null;
|
||||
profileProvider.constraints$(SomeProfile, { account$ }).subscribe({
|
||||
error: (e: unknown) => {
|
||||
actualError = e;
|
||||
},
|
||||
});
|
||||
account.error(expectedError);
|
||||
await awaitAsync();
|
||||
|
||||
expect(actualError).toEqual(expectedError);
|
||||
});
|
||||
|
||||
it("completes when the user completes", async () => {
|
||||
const profileProvider = new GeneratorProfileProvider(dependencyProvider, policyService);
|
||||
const account = new BehaviorSubject(accounts[SomeUser]);
|
||||
const account$ = account.asObservable();
|
||||
|
||||
let completed = false;
|
||||
profileProvider.constraints$(SomeProfile, { account$ }).subscribe({
|
||||
complete: () => {
|
||||
completed = true;
|
||||
},
|
||||
});
|
||||
account.complete();
|
||||
await awaitAsync();
|
||||
|
||||
expect(completed).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
Observable,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
shareReplay,
|
||||
tap,
|
||||
of,
|
||||
} from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BoundDependency } from "@bitwarden/common/tools/dependencies";
|
||||
import { SemanticLogger } from "@bitwarden/common/tools/log";
|
||||
import { anyComplete } from "@bitwarden/common/tools/rx";
|
||||
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
||||
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
|
||||
|
||||
import { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "../metadata";
|
||||
import { GeneratorConstraints } from "../types/generator-constraints";
|
||||
|
||||
/** Surfaces contextual information to credential generators */
|
||||
export class GeneratorProfileProvider {
|
||||
/** Instantiates the context provider
|
||||
* @param providers dependency injectors for user state subjects
|
||||
* @param policyService settings constraint lookups
|
||||
*/
|
||||
constructor(
|
||||
private readonly providers: UserStateSubjectDependencyProvider,
|
||||
private readonly policyService: PolicyService,
|
||||
) {
|
||||
this.log = providers.log({ type: "GeneratorProfileProvider" });
|
||||
}
|
||||
|
||||
private readonly log: SemanticLogger;
|
||||
|
||||
/** Get a subject bound to a specific user's settings for the provided profile.
|
||||
* @param profile determines which profile's settings are loaded
|
||||
* @param dependencies.singleUserId$ identifies the user to which the settings are bound
|
||||
* @returns an observable that emits the subject once `dependencies.singleUserId$` becomes
|
||||
* available and then completes.
|
||||
* @remarks the subject tracks and enforces policy on the settings it contains.
|
||||
* It completes when `dependencies.singleUserId$` competes or the user's encryption key
|
||||
* becomes unavailable.
|
||||
*/
|
||||
settings<Settings extends object>(
|
||||
profile: Readonly<CoreProfileMetadata<Settings>>,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): UserStateSubject<Settings> {
|
||||
const account$ = dependencies.account$.pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||
const constraints$ = this.constraints$(profile, { account$ });
|
||||
const subject = new UserStateSubject(profile.storage, this.providers, {
|
||||
constraints$,
|
||||
account$,
|
||||
});
|
||||
|
||||
return subject;
|
||||
}
|
||||
|
||||
/** Get the policy constraints for the provided profile
|
||||
* @param dependencies.account$ constraints are loaded from this account.
|
||||
* If the account's email is verified, it is passed to the constraints
|
||||
* @returns an observable that emits the policy once `dependencies.userId$`
|
||||
* and the policy become available.
|
||||
*/
|
||||
constraints$<Settings>(
|
||||
profile: Readonly<ProfileMetadata<Settings>>,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): Observable<GeneratorConstraints<Settings>> {
|
||||
const account$ = dependencies.account$.pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||
|
||||
const constraints$ = account$.pipe(
|
||||
distinctUntilChanged((prev, next) => {
|
||||
return prev.email === next.email && prev.emailVerified === next.emailVerified;
|
||||
}),
|
||||
switchMap((account) => {
|
||||
this.log.debug(
|
||||
{
|
||||
accountId: account.id,
|
||||
profileType: profile.type,
|
||||
policyType: profile.constraints.type ?? "N/A",
|
||||
defaultConstraints: profile.constraints.default as object,
|
||||
},
|
||||
"initializing constraints$",
|
||||
);
|
||||
|
||||
const policies$ = profile.constraints.type
|
||||
? this.policyService.getAll$(profile.constraints.type, account.id)
|
||||
: of([]);
|
||||
|
||||
const context: ProfileContext<Settings> = {
|
||||
defaultConstraints: profile.constraints.default,
|
||||
};
|
||||
if (account.emailVerified) {
|
||||
this.log.debug({ email: account.email }, "verified email detected; including in context");
|
||||
context.email = account.email;
|
||||
}
|
||||
|
||||
const constraints$ = policies$.pipe(
|
||||
map((policies) => profile.constraints.create(policies, context)),
|
||||
tap(() => this.log.debug("constraints created")),
|
||||
);
|
||||
|
||||
return constraints$;
|
||||
}),
|
||||
// complete policy emissions otherwise `switchMap` holds `constraints$`
|
||||
// open indefinitely
|
||||
takeUntil(anyComplete(account$)),
|
||||
);
|
||||
|
||||
return constraints$;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
@@ -18,9 +16,6 @@ import { AlgorithmInfo, GeneratedCredential } from "@bitwarden/generator-core";
|
||||
imports: [CommonModule, GeneratorModule],
|
||||
})
|
||||
export class CipherFormGeneratorComponent {
|
||||
@Input()
|
||||
onAlgorithmSelected: (selected: AlgorithmInfo) => void;
|
||||
|
||||
@Input()
|
||||
uri: string = "";
|
||||
|
||||
@@ -28,17 +23,25 @@ export class CipherFormGeneratorComponent {
|
||||
* The type of generator form to show.
|
||||
*/
|
||||
@Input({ required: true })
|
||||
type: "password" | "username";
|
||||
type: "password" | "username" = "password";
|
||||
|
||||
/** Removes bottom margin of internal sections */
|
||||
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
|
||||
|
||||
@Output()
|
||||
algorithmSelected = new EventEmitter<AlgorithmInfo>();
|
||||
|
||||
/**
|
||||
* Emits an event when a new value is generated.
|
||||
*/
|
||||
@Output()
|
||||
valueGenerated = new EventEmitter<string>();
|
||||
|
||||
/** Event handler for when an algorithm is selected */
|
||||
onAlgorithmSelected = (selected: AlgorithmInfo) => {
|
||||
this.algorithmSelected.emit(selected);
|
||||
};
|
||||
|
||||
/** Event handler for both generation components */
|
||||
onCredentialGenerated = (generatedCred: GeneratedCredential) => {
|
||||
this.valueGenerated.emit(generatedCred.credential);
|
||||
|
||||
Reference in New Issue
Block a user