mirror of
https://github.com/bitwarden/browser.git
synced 2025-12-05 09:14:28 +01:00
Merge 83a4d3e1df into d32365fbba
This commit is contained in:
commit
eda110e69a
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -73,6 +73,7 @@ bitwarden_license/bit-cli/src/admin-console @bitwarden/team-admin-console-dev
|
||||
libs/angular/src/admin-console @bitwarden/team-admin-console-dev
|
||||
libs/common/src/admin-console @bitwarden/team-admin-console-dev
|
||||
libs/admin-console @bitwarden/team-admin-console-dev
|
||||
libs/auto-confirm @bitwarden/team-admin-console-dev
|
||||
|
||||
## Billing team files ##
|
||||
apps/browser/src/billing @bitwarden/team-billing-dev
|
||||
|
||||
@ -4793,6 +4793,24 @@
|
||||
"adminConsole": {
|
||||
"message": "Admin Console"
|
||||
},
|
||||
"admin" :{
|
||||
"message": "Admin"
|
||||
},
|
||||
"automaticUserConfirmation": {
|
||||
"message": "Automatic user confirmation"
|
||||
},
|
||||
"automaticUserConfirmationHint": {
|
||||
"message": "Automatically confirm pending users while this device is unlocked"
|
||||
},
|
||||
"autoConfirmOnboardingCallout":{
|
||||
"message": "Save time with automatic user confirmation"
|
||||
},
|
||||
"autoConfirmWarning": {
|
||||
"message": "This could impact your organization’s data security. "
|
||||
},
|
||||
"autoConfirmWarningLink": {
|
||||
"message": "Learn about the risks"
|
||||
},
|
||||
"accountSecurity": {
|
||||
"message": "Account security"
|
||||
},
|
||||
|
||||
@ -7,6 +7,7 @@ import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { LockService } from "@bitwarden/auth/common";
|
||||
import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@ -95,6 +96,10 @@ describe("AccountSecurityComponent", () => {
|
||||
{ provide: CollectionService, useValue: mock<CollectionService>() },
|
||||
{ provide: ValidationService, useValue: validationService },
|
||||
{ provide: LockService, useValue: lockService },
|
||||
{
|
||||
provide: AutomaticUserConfirmationService,
|
||||
useValue: mock<AutomaticUserConfirmationService>(),
|
||||
},
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
],
|
||||
})
|
||||
|
||||
@ -42,6 +42,7 @@ import {
|
||||
TwoFactorAuthComponent,
|
||||
TwoFactorAuthGuard,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { canAccessAutoConfirmSettings } from "@bitwarden/auto-confirm";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
|
||||
import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
@ -86,6 +87,7 @@ import {
|
||||
} from "../vault/popup/guards/at-risk-passwords.guard";
|
||||
import { clearVaultStateGuard } from "../vault/popup/guards/clear-vault-state.guard";
|
||||
import { IntroCarouselGuard } from "../vault/popup/guards/intro-carousel.guard";
|
||||
import { AdminSettingsComponent } from "../vault/popup/settings/admin-settings.component";
|
||||
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
|
||||
import { ArchiveComponent } from "../vault/popup/settings/archive.component";
|
||||
import { DownloadBitwardenComponent } from "../vault/popup/settings/download-bitwarden.component";
|
||||
@ -315,6 +317,12 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "admin",
|
||||
component: AdminSettingsComponent,
|
||||
canActivate: [authGuard, canAccessAutoConfirmSettings],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "clone-cipher",
|
||||
component: AddEditV2Component,
|
||||
|
||||
@ -3,7 +3,11 @@
|
||||
import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
|
||||
import { merge, of, Subject } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionService,
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction";
|
||||
import { ChangePasswordService } from "@bitwarden/angular/auth/password-management/change-password";
|
||||
import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service";
|
||||
@ -38,9 +42,14 @@ import {
|
||||
LogoutService,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import {
|
||||
AutomaticUserConfirmationService,
|
||||
DefaultAutomaticUserConfirmationService,
|
||||
} from "@bitwarden/auto-confirm";
|
||||
import { ExtensionNewDeviceVerificationComponentService } from "@bitwarden/browser/auth/services/new-device-verification/extension-new-device-verification-component.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import {
|
||||
AccountService,
|
||||
@ -723,6 +732,19 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: ExtensionNewDeviceVerificationComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AutomaticUserConfirmationService,
|
||||
useClass: DefaultAutomaticUserConfirmationService,
|
||||
deps: [
|
||||
ConfigService,
|
||||
ApiService,
|
||||
OrganizationUserService,
|
||||
StateProvider,
|
||||
InternalOrganizationServiceAbstraction,
|
||||
OrganizationUserApiService,
|
||||
PolicyService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SessionTimeoutSettingsComponentService,
|
||||
useClass: BrowserSessionTimeoutSettingsComponentService,
|
||||
|
||||
@ -84,6 +84,24 @@
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
|
||||
@if (showAdminSettingsLink$ | async) {
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/admin">
|
||||
<i slot="start" class="bwi bwi-business" aria-hidden="true"></i>
|
||||
<div class="tw-flex tw-items-center tw-justify-center">
|
||||
<p class="tw-pr-2">{{ "admin" | i18n }}</p>
|
||||
@if (showAdminBadge$ | async) {
|
||||
<span bitBadge variant="notification" [attr.aria-label]="'nudgeBadgeAria' | i18n"
|
||||
>1</span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
}
|
||||
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/about">
|
||||
<i slot="start" class="bwi bwi-info-circle" aria-hidden="true"></i>
|
||||
|
||||
@ -6,6 +6,7 @@ import { BehaviorSubject, firstValueFrom, of, Subject } from "rxjs";
|
||||
|
||||
import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm";
|
||||
import { AutofillBrowserSettingsService } from "@bitwarden/browser/autofill/services/autofill-browser-settings.service";
|
||||
import { BrowserApi } from "@bitwarden/browser/platform/browser/browser-api";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@ -42,6 +43,9 @@ describe("SettingsV2Component", () => {
|
||||
defaultBrowserAutofillDisabled$: Subject<boolean>;
|
||||
isBrowserAutofillSettingOverridden: jest.Mock<Promise<boolean>>;
|
||||
};
|
||||
let mockAutoConfirmService: {
|
||||
canManageAutoConfirm$: jest.Mock;
|
||||
};
|
||||
let dialogService: MockProxy<DialogService>;
|
||||
let openSpy: jest.SpyInstance;
|
||||
|
||||
@ -66,6 +70,10 @@ describe("SettingsV2Component", () => {
|
||||
isBrowserAutofillSettingOverridden: jest.fn().mockResolvedValue(false),
|
||||
};
|
||||
|
||||
mockAutoConfirmService = {
|
||||
canManageAutoConfirm$: jest.fn().mockReturnValue(of(false)),
|
||||
};
|
||||
|
||||
jest.spyOn(BrowserApi, "getBrowserClientVendor").mockReturnValue("Chrome");
|
||||
|
||||
const cfg = TestBed.configureTestingModule({
|
||||
@ -75,6 +83,7 @@ describe("SettingsV2Component", () => {
|
||||
{ provide: BillingAccountProfileStateService, useValue: mockBillingState },
|
||||
{ provide: NudgesService, useValue: mockNudges },
|
||||
{ provide: AutofillBrowserSettingsService, useValue: mockAutofillSettings },
|
||||
{ provide: AutomaticUserConfirmationService, useValue: mockAutoConfirmService },
|
||||
{ provide: DialogService, useValue: dialogService },
|
||||
{ provide: I18nService, useValue: { t: jest.fn((key: string) => key) } },
|
||||
{ provide: GlobalStateProvider, useValue: new FakeGlobalStateProvider() },
|
||||
|
||||
@ -16,7 +16,9 @@ import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/compon
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component";
|
||||
import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
@ -82,6 +84,12 @@ export class SettingsV2Component {
|
||||
),
|
||||
);
|
||||
|
||||
showAdminBadge$: Observable<boolean> = this.authenticatedAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.nudgesService.showNudgeBadge$(NudgeType.AutoConfirmNudge, account.id),
|
||||
),
|
||||
);
|
||||
|
||||
showAutofillBadge$: Observable<boolean> = combineLatest([
|
||||
this.autofillBrowserSettingsService.defaultBrowserAutofillDisabled$,
|
||||
this.authenticatedAccount$,
|
||||
@ -95,10 +103,16 @@ export class SettingsV2Component {
|
||||
),
|
||||
);
|
||||
|
||||
showAdminSettingsLink$: Observable<boolean> = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.autoConfirmService.canManageAutoConfirm$(userId)),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private readonly nudgesService: NudgesService,
|
||||
private readonly accountService: AccountService,
|
||||
private readonly autofillBrowserSettingsService: AutofillBrowserSettingsService,
|
||||
private readonly autoConfirmService: AutomaticUserConfirmationService,
|
||||
private readonly accountProfileStateService: BillingAccountProfileStateService,
|
||||
private readonly dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
<popup-page [loading]="formLoading">
|
||||
<popup-header slot="header" [pageTitle]="'admin' | i18n" showBackButton>
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
<div class="tw-px-1 tw-pt-1">
|
||||
@if (showAutoConfirmSpotlight$ | async) {
|
||||
<bit-spotlight [persistent]="true">
|
||||
<div class="tw-flex tw-flex-row tw-items-center tw-justify-between">
|
||||
<span class="tw-text-sm">
|
||||
{{ "autoConfirmOnboardingCallout" | i18n }}
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
size="small"
|
||||
(click)="dismissSpotlight()"
|
||||
class="tw-ml-1 tw-mt-[2px]"
|
||||
[label]="'close' | i18n"
|
||||
></button>
|
||||
</div>
|
||||
</bit-spotlight>
|
||||
}
|
||||
|
||||
<form [formGroup]="adminForm">
|
||||
<bit-card>
|
||||
<bit-switch formControlName="autoConfirm">
|
||||
<bit-label>
|
||||
<span class="tw-text-sm">
|
||||
{{ "automaticUserConfirmation" | i18n }}
|
||||
</span>
|
||||
</bit-label>
|
||||
<bit-hint class="tw-max-w-[18rem]">{{ "automaticUserConfirmationHint" | i18n }}</bit-hint>
|
||||
</bit-switch>
|
||||
</bit-card>
|
||||
</form>
|
||||
</div>
|
||||
</popup-page>
|
||||
@ -0,0 +1,199 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { provideNoopAnimations } from "@angular/platform-browser/animations";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { AutoConfirmState, AutomaticUserConfirmationService } from "@bitwarden/auto-confirm";
|
||||
import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
import { AdminSettingsComponent } from "./admin-settings.component";
|
||||
|
||||
@Component({
|
||||
selector: "popup-header",
|
||||
template: `<ng-content></ng-content>`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class MockPopupHeaderComponent {
|
||||
readonly pageTitle = input<string>();
|
||||
readonly backAction = input<() => void>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "popup-page",
|
||||
template: `<ng-content></ng-content>`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class MockPopupPageComponent {
|
||||
readonly loading = input<boolean>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-pop-out",
|
||||
template: ``,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class MockPopOutComponent {
|
||||
readonly show = input<boolean>(true);
|
||||
}
|
||||
|
||||
describe("AdminSettingsComponent", () => {
|
||||
let component: AdminSettingsComponent;
|
||||
let fixture: ComponentFixture<AdminSettingsComponent>;
|
||||
let autoConfirmService: MockProxy<AutomaticUserConfirmationService>;
|
||||
let nudgesService: MockProxy<NudgesService>;
|
||||
let mockDialogService: MockProxy<DialogService>;
|
||||
|
||||
const userId = "test-user-id" as UserId;
|
||||
const mockAutoConfirmState: AutoConfirmState = {
|
||||
enabled: false,
|
||||
showSetupDialog: true,
|
||||
showBrowserNotification: false,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
autoConfirmService = mock<AutomaticUserConfirmationService>();
|
||||
nudgesService = mock<NudgesService>();
|
||||
mockDialogService = mock<DialogService>();
|
||||
|
||||
autoConfirmService.configuration$.mockReturnValue(of(mockAutoConfirmState));
|
||||
autoConfirmService.upsert.mockResolvedValue(undefined);
|
||||
nudgesService.showNudgeSpotlight$.mockReturnValue(of(false));
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AdminSettingsComponent],
|
||||
providers: [
|
||||
provideNoopAnimations(),
|
||||
{ provide: AccountService, useValue: mockAccountServiceWith(userId) },
|
||||
{ provide: AutomaticUserConfirmationService, useValue: autoConfirmService },
|
||||
{ provide: DialogService, useValue: mockDialogService },
|
||||
{ provide: NudgesService, useValue: nudgesService },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
],
|
||||
})
|
||||
.overrideComponent(AdminSettingsComponent, {
|
||||
remove: {
|
||||
imports: [PopupHeaderComponent, PopupPageComponent, PopOutComponent],
|
||||
},
|
||||
add: {
|
||||
imports: [MockPopupHeaderComponent, MockPopupPageComponent, MockPopOutComponent],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminSettingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
describe("initialization", () => {
|
||||
it("should populate form with current auto-confirm state", async () => {
|
||||
const mockState: AutoConfirmState = {
|
||||
enabled: true,
|
||||
showSetupDialog: false,
|
||||
showBrowserNotification: true,
|
||||
};
|
||||
autoConfirmService.configuration$.mockReturnValue(of(mockState));
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(component["adminForm"].value).toEqual({
|
||||
autoConfirm: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should populate form with disabled auto-confirm state", async () => {
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(component["adminForm"].value).toEqual({
|
||||
autoConfirm: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("spotlight", () => {
|
||||
beforeEach(async () => {
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should expose showAutoConfirmSpotlight$ observable", (done) => {
|
||||
nudgesService.showNudgeSpotlight$.mockReturnValue(of(true));
|
||||
|
||||
const newFixture = TestBed.createComponent(AdminSettingsComponent);
|
||||
const newComponent = newFixture.componentInstance;
|
||||
|
||||
newComponent["showAutoConfirmSpotlight$"].subscribe((show) => {
|
||||
expect(show).toBe(true);
|
||||
expect(nudgesService.showNudgeSpotlight$).toHaveBeenCalledWith(
|
||||
NudgeType.AutoConfirmNudge,
|
||||
userId,
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should dismiss spotlight and update state", async () => {
|
||||
autoConfirmService.upsert.mockResolvedValue();
|
||||
|
||||
await component.dismissSpotlight();
|
||||
|
||||
expect(autoConfirmService.upsert).toHaveBeenCalledWith(userId, {
|
||||
...mockAutoConfirmState,
|
||||
showBrowserNotification: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should use current userId when dismissing spotlight", async () => {
|
||||
autoConfirmService.upsert.mockResolvedValue();
|
||||
|
||||
await component.dismissSpotlight();
|
||||
|
||||
expect(autoConfirmService.upsert).toHaveBeenCalledWith(userId, expect.any(Object));
|
||||
});
|
||||
|
||||
it("should preserve existing state when dismissing spotlight", async () => {
|
||||
const customState: AutoConfirmState = {
|
||||
enabled: true,
|
||||
showSetupDialog: false,
|
||||
showBrowserNotification: true,
|
||||
};
|
||||
autoConfirmService.configuration$.mockReturnValue(of(customState));
|
||||
autoConfirmService.upsert.mockResolvedValue();
|
||||
|
||||
await component.dismissSpotlight();
|
||||
|
||||
expect(autoConfirmService.upsert).toHaveBeenCalledWith(userId, {
|
||||
...customState,
|
||||
showBrowserNotification: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("form validation", () => {
|
||||
beforeEach(async () => {
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should have a valid form", () => {
|
||||
expect(component["adminForm"].valid).toBe(true);
|
||||
});
|
||||
|
||||
it("should have autoConfirm control", () => {
|
||||
expect(component["adminForm"].controls.autoConfirm).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,109 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, DestroyRef, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { firstValueFrom, lastValueFrom, Observable, switchMap, withLatestFrom } from "rxjs";
|
||||
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component";
|
||||
import {
|
||||
AutoConfirmWarningDialogComponent,
|
||||
AutomaticUserConfirmationService,
|
||||
} from "@bitwarden/auto-confirm";
|
||||
import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component";
|
||||
import { PopupHeaderComponent } from "@bitwarden/browser/platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "@bitwarden/browser/platform/popup/layout/popup-page.component";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import {
|
||||
BitIconButtonComponent,
|
||||
CardComponent,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
SwitchComponent,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./admin-settings.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
PopOutComponent,
|
||||
FormFieldModule,
|
||||
ReactiveFormsModule,
|
||||
SwitchComponent,
|
||||
CardComponent,
|
||||
SpotlightComponent,
|
||||
BitIconButtonComponent,
|
||||
I18nPipe,
|
||||
],
|
||||
})
|
||||
export class AdminSettingsComponent implements OnInit {
|
||||
private userId$: Observable<UserId> = this.accountService.activeAccount$.pipe(getUserId);
|
||||
|
||||
protected formLoading = true;
|
||||
protected adminForm = this.formBuilder.group({
|
||||
autoConfirm: false,
|
||||
});
|
||||
protected showAutoConfirmSpotlight$: Observable<boolean> = this.userId$.pipe(
|
||||
switchMap((userId) =>
|
||||
this.nudgesService.showNudgeSpotlight$(NudgeType.AutoConfirmNudge, userId),
|
||||
),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private accountService: AccountService,
|
||||
private autoConfirmService: AutomaticUserConfirmationService,
|
||||
private destroyRef: DestroyRef,
|
||||
private dialogService: DialogService,
|
||||
private nudgesService: NudgesService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.formLoading = false;
|
||||
|
||||
const userId = await firstValueFrom(this.userId$);
|
||||
const autoConfirmEnabled = (
|
||||
await firstValueFrom(this.autoConfirmService.configuration$(userId))
|
||||
).enabled;
|
||||
this.adminForm.setValue({ autoConfirm: autoConfirmEnabled });
|
||||
|
||||
this.adminForm.controls.autoConfirm.valueChanges
|
||||
.pipe(
|
||||
switchMap(async (newValue) => {
|
||||
if (newValue) {
|
||||
const ref = AutoConfirmWarningDialogComponent.open(this.dialogService);
|
||||
const result = await lastValueFrom(ref.closed);
|
||||
|
||||
if (result) {
|
||||
return newValue;
|
||||
}
|
||||
}
|
||||
this.adminForm.setValue({ autoConfirm: false }, { emitEvent: false });
|
||||
return false;
|
||||
}),
|
||||
withLatestFrom(this.autoConfirmService.configuration$(userId)),
|
||||
switchMap(([newValue, existingState]) =>
|
||||
this.autoConfirmService.upsert(userId, {
|
||||
...existingState,
|
||||
enabled: newValue,
|
||||
showBrowserNotification: false,
|
||||
}),
|
||||
),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async dismissSpotlight() {
|
||||
const userId = await firstValueFrom(this.userId$);
|
||||
const state = await firstValueFrom(this.autoConfirmService.configuration$(userId));
|
||||
|
||||
await this.autoConfirmService.upsert(userId, { ...state, showBrowserNotification: false });
|
||||
}
|
||||
}
|
||||
@ -22,7 +22,7 @@ import {
|
||||
tap,
|
||||
} from "rxjs";
|
||||
|
||||
import { AutomaticUserConfirmationService } from "@bitwarden/admin-console/common";
|
||||
import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
|
||||
@ -188,7 +188,7 @@ describe("PoliciesComponent", () => {
|
||||
});
|
||||
|
||||
describe("orgPolicies$", () => {
|
||||
it("should fetch policies from API for current organization", async () => {
|
||||
describe("with multiple policies", () => {
|
||||
const mockPolicyResponsesData = [
|
||||
{
|
||||
id: newGuid(),
|
||||
@ -206,39 +206,63 @@ describe("PoliciesComponent", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const listResponse = new ListResponse(
|
||||
{ Data: mockPolicyResponsesData, ContinuationToken: null },
|
||||
PolicyResponse,
|
||||
);
|
||||
beforeEach(async () => {
|
||||
const listResponse = new ListResponse(
|
||||
{ Data: mockPolicyResponsesData, ContinuationToken: null },
|
||||
PolicyResponse,
|
||||
);
|
||||
|
||||
mockPolicyApiService.getPolicies.mockResolvedValue(listResponse);
|
||||
mockPolicyApiService.getPolicies.mockResolvedValue(listResponse);
|
||||
|
||||
const policies = await firstValueFrom(component["orgPolicies$"]);
|
||||
expect(policies).toEqual(listResponse.data);
|
||||
expect(mockPolicyApiService.getPolicies).toHaveBeenCalledWith(mockOrgId);
|
||||
fixture = TestBed.createComponent(PoliciesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should fetch policies from API for current organization", async () => {
|
||||
const policies = await firstValueFrom(component["orgPolicies$"]);
|
||||
expect(policies.length).toBe(2);
|
||||
expect(mockPolicyApiService.getPolicies).toHaveBeenCalledWith(mockOrgId);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty array when API returns no data", async () => {
|
||||
mockPolicyApiService.getPolicies.mockResolvedValue(
|
||||
new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse),
|
||||
);
|
||||
describe("with no policies", () => {
|
||||
beforeEach(async () => {
|
||||
mockPolicyApiService.getPolicies.mockResolvedValue(
|
||||
new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse),
|
||||
);
|
||||
|
||||
const policies = await firstValueFrom(component["orgPolicies$"]);
|
||||
expect(policies).toEqual([]);
|
||||
fixture = TestBed.createComponent(PoliciesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should return empty array when API returns no data", async () => {
|
||||
const policies = await firstValueFrom(component["orgPolicies$"]);
|
||||
expect(policies).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty array when API returns null data", async () => {
|
||||
mockPolicyApiService.getPolicies.mockResolvedValue(
|
||||
new ListResponse({ Data: null, ContinuationToken: null }, PolicyResponse),
|
||||
);
|
||||
describe("with null data", () => {
|
||||
beforeEach(async () => {
|
||||
mockPolicyApiService.getPolicies.mockResolvedValue(
|
||||
new ListResponse({ Data: null, ContinuationToken: null }, PolicyResponse),
|
||||
);
|
||||
|
||||
const policies = await firstValueFrom(component["orgPolicies$"]);
|
||||
expect(policies).toEqual([]);
|
||||
fixture = TestBed.createComponent(PoliciesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should return empty array when API returns null data", async () => {
|
||||
const policies = await firstValueFrom(component["orgPolicies$"]);
|
||||
expect(policies).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("policiesEnabledMap$", () => {
|
||||
it("should create a map of policy types to their enabled status", async () => {
|
||||
describe("with multiple policies", () => {
|
||||
const mockPolicyResponsesData = [
|
||||
{
|
||||
id: "policy-1",
|
||||
@ -263,27 +287,43 @@ describe("PoliciesComponent", () => {
|
||||
},
|
||||
];
|
||||
|
||||
mockPolicyApiService.getPolicies.mockResolvedValue(
|
||||
new ListResponse(
|
||||
{ Data: mockPolicyResponsesData, ContinuationToken: null },
|
||||
PolicyResponse,
|
||||
),
|
||||
);
|
||||
beforeEach(async () => {
|
||||
mockPolicyApiService.getPolicies.mockResolvedValue(
|
||||
new ListResponse(
|
||||
{ Data: mockPolicyResponsesData, ContinuationToken: null },
|
||||
PolicyResponse,
|
||||
),
|
||||
);
|
||||
|
||||
const map = await firstValueFrom(component.policiesEnabledMap$);
|
||||
expect(map.size).toBe(3);
|
||||
expect(map.get(PolicyType.TwoFactorAuthentication)).toBe(true);
|
||||
expect(map.get(PolicyType.RequireSso)).toBe(false);
|
||||
expect(map.get(PolicyType.SingleOrg)).toBe(true);
|
||||
fixture = TestBed.createComponent(PoliciesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create a map of policy types to their enabled status", async () => {
|
||||
const map = await firstValueFrom(component.policiesEnabledMap$);
|
||||
expect(map.size).toBe(3);
|
||||
expect(map.get(PolicyType.TwoFactorAuthentication)).toBe(true);
|
||||
expect(map.get(PolicyType.RequireSso)).toBe(false);
|
||||
expect(map.get(PolicyType.SingleOrg)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create empty map when no policies exist", async () => {
|
||||
mockPolicyApiService.getPolicies.mockResolvedValue(
|
||||
new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse),
|
||||
);
|
||||
describe("with no policies", () => {
|
||||
beforeEach(async () => {
|
||||
mockPolicyApiService.getPolicies.mockResolvedValue(
|
||||
new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse),
|
||||
);
|
||||
|
||||
const map = await firstValueFrom(component.policiesEnabledMap$);
|
||||
expect(map.size).toBe(0);
|
||||
fixture = TestBed.createComponent(PoliciesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create empty map when no policies exist", async () => {
|
||||
const map = await firstValueFrom(component.policiesEnabledMap$);
|
||||
expect(map.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -292,31 +332,36 @@ describe("PoliciesComponent", () => {
|
||||
expect(mockPolicyService.policies$).toHaveBeenCalledWith(mockUserId);
|
||||
});
|
||||
|
||||
it("should refresh policies when policyService emits", async () => {
|
||||
const policiesSubject = new BehaviorSubject<any[]>([]);
|
||||
mockPolicyService.policies$.mockReturnValue(policiesSubject.asObservable());
|
||||
describe("when policyService emits", () => {
|
||||
let policiesSubject: BehaviorSubject<any[]>;
|
||||
let callCount: number;
|
||||
|
||||
let callCount = 0;
|
||||
mockPolicyApiService.getPolicies.mockImplementation(() => {
|
||||
callCount++;
|
||||
return of(new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse));
|
||||
beforeEach(async () => {
|
||||
policiesSubject = new BehaviorSubject<any[]>([]);
|
||||
mockPolicyService.policies$.mockReturnValue(policiesSubject.asObservable());
|
||||
|
||||
callCount = 0;
|
||||
mockPolicyApiService.getPolicies.mockImplementation(() => {
|
||||
callCount++;
|
||||
return of(new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse));
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(PoliciesComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
const newFixture = TestBed.createComponent(PoliciesComponent);
|
||||
newFixture.detectChanges();
|
||||
it("should refresh policies when policyService emits", () => {
|
||||
const initialCallCount = callCount;
|
||||
|
||||
const initialCallCount = callCount;
|
||||
policiesSubject.next([{ type: PolicyType.TwoFactorAuthentication }]);
|
||||
|
||||
policiesSubject.next([{ type: PolicyType.TwoFactorAuthentication }]);
|
||||
|
||||
expect(callCount).toBeGreaterThan(initialCallCount);
|
||||
|
||||
newFixture.destroy();
|
||||
expect(callCount).toBeGreaterThan(initialCallCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleLaunchEvent", () => {
|
||||
it("should open policy dialog when policyId is in query params", async () => {
|
||||
describe("when policyId is in query params", () => {
|
||||
const mockPolicyId = newGuid();
|
||||
const mockPolicy: BasePolicyEditDefinition = {
|
||||
name: "Test Policy",
|
||||
@ -335,54 +380,59 @@ describe("PoliciesComponent", () => {
|
||||
data: null,
|
||||
};
|
||||
|
||||
queryParamsSubject.next({ policyId: mockPolicyId });
|
||||
let dialogOpenSpy: jest.SpyInstance;
|
||||
|
||||
mockPolicyApiService.getPolicies.mockReturnValue(
|
||||
of(
|
||||
new ListResponse(
|
||||
{ Data: [mockPolicyResponseData], ContinuationToken: null },
|
||||
PolicyResponse,
|
||||
beforeEach(async () => {
|
||||
queryParamsSubject.next({ policyId: mockPolicyId });
|
||||
|
||||
mockPolicyApiService.getPolicies.mockReturnValue(
|
||||
of(
|
||||
new ListResponse(
|
||||
{ Data: [mockPolicyResponseData], ContinuationToken: null },
|
||||
PolicyResponse,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
|
||||
const dialogOpenSpy = jest
|
||||
.spyOn(PolicyEditDialogComponent, "open")
|
||||
.mockReturnValue({ close: jest.fn() } as any);
|
||||
dialogOpenSpy = jest
|
||||
.spyOn(PolicyEditDialogComponent, "open")
|
||||
.mockReturnValue({ close: jest.fn() } as any);
|
||||
|
||||
TestBed.resetTestingModule();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PoliciesComponent],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
|
||||
{ provide: OrganizationService, useValue: mockOrganizationService },
|
||||
{ provide: AccountService, useValue: mockAccountService },
|
||||
{ provide: PolicyApiServiceAbstraction, useValue: mockPolicyApiService },
|
||||
{ provide: PolicyListService, useValue: mockPolicyListService },
|
||||
{ provide: DialogService, useValue: mockDialogService },
|
||||
{ provide: PolicyService, useValue: mockPolicyService },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
|
||||
{ provide: POLICY_EDIT_REGISTER, useValue: [mockPolicy] },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
})
|
||||
.overrideComponent(PoliciesComponent, {
|
||||
remove: { imports: [] },
|
||||
add: { template: "<div></div>" },
|
||||
TestBed.resetTestingModule();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PoliciesComponent],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
|
||||
{ provide: OrganizationService, useValue: mockOrganizationService },
|
||||
{ provide: AccountService, useValue: mockAccountService },
|
||||
{ provide: PolicyApiServiceAbstraction, useValue: mockPolicyApiService },
|
||||
{ provide: PolicyListService, useValue: mockPolicyListService },
|
||||
{ provide: DialogService, useValue: mockDialogService },
|
||||
{ provide: PolicyService, useValue: mockPolicyService },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
|
||||
{ provide: POLICY_EDIT_REGISTER, useValue: [mockPolicy] },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
})
|
||||
.compileComponents();
|
||||
.overrideComponent(PoliciesComponent, {
|
||||
remove: { imports: [] },
|
||||
add: { template: "<div></div>" },
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
const newFixture = TestBed.createComponent(PoliciesComponent);
|
||||
newFixture.detectChanges();
|
||||
fixture = TestBed.createComponent(PoliciesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
expect(dialogOpenSpy).toHaveBeenCalled();
|
||||
const callArgs = dialogOpenSpy.mock.calls[0][1];
|
||||
expect(callArgs.data?.policy.type).toBe(mockPolicy.type);
|
||||
expect(callArgs.data?.organizationId).toBe(mockOrgId);
|
||||
|
||||
newFixture.destroy();
|
||||
it("should open policy dialog when policyId is in query params", () => {
|
||||
expect(dialogOpenSpy).toHaveBeenCalled();
|
||||
const callArgs = dialogOpenSpy.mock.calls[0][1];
|
||||
expect(callArgs.data?.policy.type).toBe(mockPolicy.type);
|
||||
expect(callArgs.data?.organizationId).toBe(mockOrgId);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not open dialog when policyId is not in query params", async () => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, DestroyRef } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { combineLatest, Observable, of, switchMap, first, map } from "rxjs";
|
||||
import { combineLatest, Observable, of, switchMap, first, map, shareReplay } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
@ -70,6 +70,7 @@ export class PoliciesComponent {
|
||||
switchMap(() => this.organizationId$),
|
||||
switchMap((organizationId) => this.policyApiService.getPolicies(organizationId)),
|
||||
map((response) => (response.data != null && response.data.length > 0 ? response.data : [])),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
protected policiesEnabledMap$: Observable<Map<PolicyType, boolean>> = this.orgPolicies$.pipe(
|
||||
|
||||
@ -9,8 +9,6 @@ import {
|
||||
DefaultCollectionAdminService,
|
||||
OrganizationUserApiService,
|
||||
CollectionService,
|
||||
AutomaticUserConfirmationService,
|
||||
DefaultAutomaticUserConfirmationService,
|
||||
OrganizationUserService,
|
||||
DefaultOrganizationUserService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
@ -46,6 +44,10 @@ import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
LoginEmailService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import {
|
||||
AutomaticUserConfirmationService,
|
||||
DefaultAutomaticUserConfirmationService,
|
||||
} from "@bitwarden/auto-confirm";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import {
|
||||
@ -370,6 +372,7 @@ const safeProviders: SafeProvider[] = [
|
||||
StateProvider,
|
||||
InternalOrganizationServiceAbstraction,
|
||||
OrganizationUserApiService,
|
||||
PolicyService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { Route, RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { organizationPolicyGuard } from "@bitwarden/angular/admin-console/guards";
|
||||
import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component";
|
||||
import { AuthRoute } from "@bitwarden/angular/auth/constants";
|
||||
import {
|
||||
@ -56,7 +57,6 @@ import { premiumInterestRedirectGuard } from "@bitwarden/web-vault/app/vault/gua
|
||||
|
||||
import { flagEnabled, Flags } from "../utils/flags";
|
||||
|
||||
import { organizationPolicyGuard } from "./admin-console/organizations/guards/org-policy.guard";
|
||||
import { VerifyRecoverDeleteOrgComponent } from "./admin-console/organizations/manage/verify-recover-delete-org.component";
|
||||
import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/sponsorships/accept-family-sponsorship.component";
|
||||
import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component";
|
||||
|
||||
@ -26,7 +26,6 @@ import {
|
||||
} from "rxjs/operators";
|
||||
|
||||
import {
|
||||
AutomaticUserConfirmationService,
|
||||
CollectionData,
|
||||
CollectionDetailsResponse,
|
||||
CollectionService,
|
||||
@ -42,6 +41,7 @@ import {
|
||||
ItemTypes,
|
||||
Icon,
|
||||
} from "@bitwarden/assets/svg";
|
||||
import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import {
|
||||
|
||||
@ -59,6 +59,7 @@ module.exports = {
|
||||
"<rootDir>/libs/tools/send/send-ui/jest.config.js",
|
||||
"<rootDir>/libs/user-core/jest.config.js",
|
||||
"<rootDir>/libs/vault/jest.config.js",
|
||||
"<rootDir>/libs/auto-confirm/jest.config.js",
|
||||
],
|
||||
|
||||
// Workaround for a memory leak that crashes tests in CI:
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
export * from "./auto-confirm";
|
||||
export * from "./collections";
|
||||
export * from "./organization-user";
|
||||
|
||||
1
libs/angular/src/admin-console/guards/index.ts
Normal file
1
libs/angular/src/admin-console/guards/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./org-policy.guard";
|
||||
@ -0,0 +1,226 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../../../../libs/common/spec";
|
||||
import { NUDGE_DISMISSED_DISK_KEY, NudgeType } from "../nudges.service";
|
||||
|
||||
import { AutoConfirmNudgeService } from "./auto-confirm-nudge.service";
|
||||
|
||||
describe("AutoConfirmNudgeService", () => {
|
||||
let service: AutoConfirmNudgeService;
|
||||
let autoConfirmService: MockProxy<AutomaticUserConfirmationService>;
|
||||
let fakeStateProvider: FakeStateProvider;
|
||||
const userId = "user-id" as UserId;
|
||||
|
||||
const mockAutoConfirmState = {
|
||||
enabled: true,
|
||||
showSetupDialog: false,
|
||||
showBrowserNotification: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
|
||||
autoConfirmService = mock<AutomaticUserConfirmationService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
AutoConfirmNudgeService,
|
||||
{
|
||||
provide: StateProvider,
|
||||
useValue: fakeStateProvider,
|
||||
},
|
||||
{
|
||||
provide: AutomaticUserConfirmationService,
|
||||
useValue: autoConfirmService,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(AutoConfirmNudgeService);
|
||||
});
|
||||
|
||||
describe("nudgeStatus$", () => {
|
||||
it("should return all dismissed when user cannot manage auto-confirm", async () => {
|
||||
autoConfirmService.configuration$.mockReturnValue(new BehaviorSubject(mockAutoConfirmState));
|
||||
autoConfirmService.canManageAutoConfirm$.mockReturnValue(new BehaviorSubject(false));
|
||||
|
||||
const result = await firstValueFrom(service.nudgeStatus$(NudgeType.AutoConfirmNudge, userId));
|
||||
|
||||
expect(result).toEqual({
|
||||
hasBadgeDismissed: true,
|
||||
hasSpotlightDismissed: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return all dismissed when showBrowserNotification is false", async () => {
|
||||
autoConfirmService.configuration$.mockReturnValue(
|
||||
new BehaviorSubject({
|
||||
...mockAutoConfirmState,
|
||||
showBrowserNotification: false,
|
||||
}),
|
||||
);
|
||||
autoConfirmService.canManageAutoConfirm$.mockReturnValue(new BehaviorSubject(true));
|
||||
|
||||
const result = await firstValueFrom(service.nudgeStatus$(NudgeType.AutoConfirmNudge, userId));
|
||||
|
||||
expect(result).toEqual({
|
||||
hasBadgeDismissed: true,
|
||||
hasSpotlightDismissed: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return not dismissed when showBrowserNotification is true and user can manage", async () => {
|
||||
autoConfirmService.configuration$.mockReturnValue(
|
||||
new BehaviorSubject({
|
||||
...mockAutoConfirmState,
|
||||
showBrowserNotification: true,
|
||||
}),
|
||||
);
|
||||
autoConfirmService.canManageAutoConfirm$.mockReturnValue(new BehaviorSubject(true));
|
||||
|
||||
const result = await firstValueFrom(service.nudgeStatus$(NudgeType.AutoConfirmNudge, userId));
|
||||
|
||||
expect(result).toEqual({
|
||||
hasBadgeDismissed: false,
|
||||
hasSpotlightDismissed: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return not dismissed when showBrowserNotification is undefined and user can manage", async () => {
|
||||
autoConfirmService.configuration$.mockReturnValue(
|
||||
new BehaviorSubject({
|
||||
...mockAutoConfirmState,
|
||||
showBrowserNotification: undefined,
|
||||
}),
|
||||
);
|
||||
autoConfirmService.canManageAutoConfirm$.mockReturnValue(new BehaviorSubject(true));
|
||||
|
||||
const result = await firstValueFrom(service.nudgeStatus$(NudgeType.AutoConfirmNudge, userId));
|
||||
|
||||
expect(result).toEqual({
|
||||
hasBadgeDismissed: false,
|
||||
hasSpotlightDismissed: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return stored nudge status when badge is already dismissed", async () => {
|
||||
await fakeStateProvider.getUser(userId, NUDGE_DISMISSED_DISK_KEY).update(() => ({
|
||||
[NudgeType.AutoConfirmNudge]: {
|
||||
hasBadgeDismissed: true,
|
||||
hasSpotlightDismissed: false,
|
||||
},
|
||||
}));
|
||||
|
||||
autoConfirmService.configuration$.mockReturnValue(
|
||||
new BehaviorSubject({
|
||||
...mockAutoConfirmState,
|
||||
showBrowserNotification: true,
|
||||
}),
|
||||
);
|
||||
autoConfirmService.canManageAutoConfirm$.mockReturnValue(new BehaviorSubject(true));
|
||||
|
||||
const result = await firstValueFrom(service.nudgeStatus$(NudgeType.AutoConfirmNudge, userId));
|
||||
|
||||
expect(result).toEqual({
|
||||
hasBadgeDismissed: true,
|
||||
hasSpotlightDismissed: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return stored nudge status when spotlight is already dismissed", async () => {
|
||||
await fakeStateProvider.getUser(userId, NUDGE_DISMISSED_DISK_KEY).update(() => ({
|
||||
[NudgeType.AutoConfirmNudge]: {
|
||||
hasBadgeDismissed: false,
|
||||
hasSpotlightDismissed: true,
|
||||
},
|
||||
}));
|
||||
|
||||
autoConfirmService.configuration$.mockReturnValue(
|
||||
new BehaviorSubject({
|
||||
...mockAutoConfirmState,
|
||||
showBrowserNotification: true,
|
||||
}),
|
||||
);
|
||||
autoConfirmService.canManageAutoConfirm$.mockReturnValue(new BehaviorSubject(true));
|
||||
|
||||
const result = await firstValueFrom(service.nudgeStatus$(NudgeType.AutoConfirmNudge, userId));
|
||||
|
||||
expect(result).toEqual({
|
||||
hasBadgeDismissed: false,
|
||||
hasSpotlightDismissed: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return stored nudge status when both badge and spotlight are already dismissed", async () => {
|
||||
await fakeStateProvider.getUser(userId, NUDGE_DISMISSED_DISK_KEY).update(() => ({
|
||||
[NudgeType.AutoConfirmNudge]: {
|
||||
hasBadgeDismissed: true,
|
||||
hasSpotlightDismissed: true,
|
||||
},
|
||||
}));
|
||||
|
||||
autoConfirmService.configuration$.mockReturnValue(
|
||||
new BehaviorSubject({
|
||||
...mockAutoConfirmState,
|
||||
showBrowserNotification: true,
|
||||
}),
|
||||
);
|
||||
autoConfirmService.canManageAutoConfirm$.mockReturnValue(new BehaviorSubject(true));
|
||||
|
||||
const result = await firstValueFrom(service.nudgeStatus$(NudgeType.AutoConfirmNudge, userId));
|
||||
|
||||
expect(result).toEqual({
|
||||
hasBadgeDismissed: true,
|
||||
hasSpotlightDismissed: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should prioritize user permissions over showBrowserNotification setting", async () => {
|
||||
await fakeStateProvider.getUser(userId, NUDGE_DISMISSED_DISK_KEY).update(() => ({
|
||||
[NudgeType.AutoConfirmNudge]: {
|
||||
hasBadgeDismissed: false,
|
||||
hasSpotlightDismissed: false,
|
||||
},
|
||||
}));
|
||||
|
||||
autoConfirmService.configuration$.mockReturnValue(
|
||||
new BehaviorSubject({
|
||||
...mockAutoConfirmState,
|
||||
showBrowserNotification: true,
|
||||
}),
|
||||
);
|
||||
autoConfirmService.canManageAutoConfirm$.mockReturnValue(new BehaviorSubject(false));
|
||||
|
||||
const result = await firstValueFrom(service.nudgeStatus$(NudgeType.AutoConfirmNudge, userId));
|
||||
|
||||
expect(result).toEqual({
|
||||
hasBadgeDismissed: true,
|
||||
hasSpotlightDismissed: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should respect stored dismissal even when user cannot manage auto-confirm", async () => {
|
||||
await fakeStateProvider.getUser(userId, NUDGE_DISMISSED_DISK_KEY).update(() => ({
|
||||
[NudgeType.AutoConfirmNudge]: {
|
||||
hasBadgeDismissed: true,
|
||||
hasSpotlightDismissed: false,
|
||||
},
|
||||
}));
|
||||
|
||||
autoConfirmService.configuration$.mockReturnValue(new BehaviorSubject(mockAutoConfirmState));
|
||||
autoConfirmService.canManageAutoConfirm$.mockReturnValue(new BehaviorSubject(false));
|
||||
|
||||
const result = await firstValueFrom(service.nudgeStatus$(NudgeType.AutoConfirmNudge, userId));
|
||||
|
||||
expect(result).toEqual({
|
||||
hasBadgeDismissed: true,
|
||||
hasSpotlightDismissed: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,41 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { combineLatest, map, Observable } from "rxjs";
|
||||
|
||||
import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
import { NudgeType, NudgeStatus } from "../nudges.service";
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class AutoConfirmNudgeService extends DefaultSingleNudgeService {
|
||||
autoConfirmService = inject(AutomaticUserConfirmationService);
|
||||
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
return combineLatest([
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
this.autoConfirmService.configuration$(userId),
|
||||
this.autoConfirmService.canManageAutoConfirm$(userId),
|
||||
]).pipe(
|
||||
map(([nudgeStatus, autoConfirmState, canManageAutoConfirm]) => {
|
||||
if (!canManageAutoConfirm) {
|
||||
return {
|
||||
hasBadgeDismissed: true,
|
||||
hasSpotlightDismissed: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (nudgeStatus.hasBadgeDismissed || nudgeStatus.hasSpotlightDismissed) {
|
||||
return nudgeStatus;
|
||||
}
|
||||
|
||||
const dismissed = autoConfirmState.showBrowserNotification === false;
|
||||
|
||||
return {
|
||||
hasBadgeDismissed: dismissed,
|
||||
hasSpotlightDismissed: dismissed,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -4,3 +4,4 @@ export * from "./empty-vault-nudge.service";
|
||||
export * from "./vault-settings-import-nudge.service";
|
||||
export * from "./new-item-nudge.service";
|
||||
export * from "./new-account-nudge.service";
|
||||
export * from "./auto-confirm-nudge.service";
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
AccountSecurityNudgeService,
|
||||
VaultSettingsImportNudgeService,
|
||||
} from "./custom-nudges-services";
|
||||
import { AutoConfirmNudgeService } from "./custom-nudges-services/auto-confirm-nudge.service";
|
||||
import { DefaultSingleNudgeService } from "./default-single-nudge.service";
|
||||
import { NudgesService, NudgeType } from "./nudges.service";
|
||||
|
||||
@ -35,6 +36,7 @@ describe("Vault Nudges Service", () => {
|
||||
EmptyVaultNudgeService,
|
||||
NewAccountNudgeService,
|
||||
AccountSecurityNudgeService,
|
||||
AutoConfirmNudgeService,
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -73,6 +75,10 @@ describe("Vault Nudges Service", () => {
|
||||
provide: VaultSettingsImportNudgeService,
|
||||
useValue: mock<VaultSettingsImportNudgeService>(),
|
||||
},
|
||||
{
|
||||
provide: AutoConfirmNudgeService,
|
||||
useValue: mock<AutoConfirmNudgeService>(),
|
||||
},
|
||||
{
|
||||
provide: ApiService,
|
||||
useValue: mock<ApiService>(),
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
NewItemNudgeService,
|
||||
AccountSecurityNudgeService,
|
||||
VaultSettingsImportNudgeService,
|
||||
AutoConfirmNudgeService,
|
||||
} from "./custom-nudges-services";
|
||||
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
|
||||
|
||||
@ -37,6 +38,7 @@ export const NudgeType = {
|
||||
NewNoteItemStatus: "new-note-item-status",
|
||||
NewSshItemStatus: "new-ssh-item-status",
|
||||
GeneratorNudgeStatus: "generator-nudge-status",
|
||||
AutoConfirmNudge: "auto-confirm-nudge",
|
||||
PremiumUpgrade: "premium-upgrade",
|
||||
} as const;
|
||||
|
||||
@ -74,6 +76,7 @@ export class NudgesService {
|
||||
[NudgeType.NewIdentityItemStatus]: this.newItemNudgeService,
|
||||
[NudgeType.NewNoteItemStatus]: this.newItemNudgeService,
|
||||
[NudgeType.NewSshItemStatus]: this.newItemNudgeService,
|
||||
[NudgeType.AutoConfirmNudge]: inject(AutoConfirmNudgeService),
|
||||
};
|
||||
|
||||
/**
|
||||
@ -140,6 +143,7 @@ export class NudgesService {
|
||||
NudgeType.EmptyVaultNudge,
|
||||
NudgeType.DownloadBitwarden,
|
||||
NudgeType.AutofillNudge,
|
||||
NudgeType.AutoConfirmNudge,
|
||||
];
|
||||
|
||||
const nudgeTypesWithBadge$ = nudgeTypes.map((nudge) => {
|
||||
|
||||
18
libs/auto-confirm/README.md
Normal file
18
libs/auto-confirm/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Automatic User Confirmation
|
||||
|
||||
Owned by: admin-console
|
||||
|
||||
The automatic user confirmation (auto confirm) feature enables an organization to confirm users to an organization without manual intervention
|
||||
from any user as long as an administrator's device is unlocked. The feature is enabled via the following:
|
||||
|
||||
1. an organization plan feature in the Bitwarden Portal (enabled by an internal team)
|
||||
2. the automatic user confirmation policy in the Admin Console (enabled by an organization admin)
|
||||
3. a toggle switch in the extension's admin settings page (enabled on the admin's local device)
|
||||
|
||||
Once these three toggles are enabled, auto confirm will be enabled and users will be auto confirmed as long as an admin is logged in. Note that the setting in
|
||||
the browser extension is not synced across clients, therefore it will not be enabled if the same admin logs into another browser until it is enabled in that
|
||||
browser. This is an intentional security measure to ensure that the server cannot enable the feature unilaterally.
|
||||
|
||||
Once enabled, the AutomaticUserConfirmationService runs in the background on admins' devices and reacts to push notifications from the server containing organization members who need confirmation.
|
||||
|
||||
For more information about security goals and the push notification system, see [README in server repo](https://github.com/bitwarden/server/tree/main/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser).
|
||||
3
libs/auto-confirm/eslint.config.mjs
Normal file
3
libs/auto-confirm/eslint.config.mjs
Normal file
@ -0,0 +1,3 @@
|
||||
import baseConfig from "../../eslint.config.mjs";
|
||||
|
||||
export default [...baseConfig];
|
||||
18
libs/auto-confirm/jest.config.js
Normal file
18
libs/auto-confirm/jest.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
|
||||
const { compilerOptions } = require("../../tsconfig.base");
|
||||
|
||||
const sharedConfig = require("../../libs/shared/jest.config.angular");
|
||||
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
displayName: "auto-confirm",
|
||||
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||
coverageDirectory: "../../coverage/libs/auto-confirm",
|
||||
moduleNameMapper: pathsToModuleNameMapper(
|
||||
{ "@bitwarden/common/spec": ["libs/common/spec"], ...(compilerOptions?.paths ?? {}) },
|
||||
{
|
||||
prefix: "<rootDir>/../../",
|
||||
},
|
||||
),
|
||||
};
|
||||
11
libs/auto-confirm/package.json
Normal file
11
libs/auto-confirm/package.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@bitwarden/auto-confirm",
|
||||
"version": "0.0.1",
|
||||
"description": "auto confirm",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"license": "GPL-3.0",
|
||||
"author": "admin-console"
|
||||
}
|
||||
34
libs/auto-confirm/project.json
Normal file
34
libs/auto-confirm/project.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "auto-confirm",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/auto-confirm/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/js:tsc",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/auto-confirm",
|
||||
"main": "libs/auto-confirm/src/index.ts",
|
||||
"tsConfig": "libs/auto-confirm/tsconfig.lib.json",
|
||||
"assets": ["libs/auto-confirm/*.md"],
|
||||
"rootDir": "libs/auto-confirm/src"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["libs/auto-confirm/**/*.ts"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/auto-confirm/jest.config.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { AutoConfirmState } from "../models/auto-confirm-state.model";
|
||||
@ -24,10 +23,7 @@ export abstract class AutomaticUserConfirmationService {
|
||||
* @param userId
|
||||
* @returns Observable<boolean> an observable with a boolean telling us if the provided user may confgure the auto confirm feature.
|
||||
**/
|
||||
abstract canManageAutoConfirm$(
|
||||
userId: UserId,
|
||||
organizationId: OrganizationId,
|
||||
): Observable<boolean>;
|
||||
abstract canManageAutoConfirm$(userId: UserId): Observable<boolean>;
|
||||
/**
|
||||
* Calls the API endpoint to initiate automatic user confirmation.
|
||||
* @param userId The userId of the logged in admin performing auto confirmation. This is neccesary to perform the key exchange and for permissions checks.
|
||||
@ -0,0 +1,20 @@
|
||||
<bit-simple-dialog dialogSize="small">
|
||||
<span bitDialogTitle>
|
||||
<strong>{{ "warningCapitalized" | i18n }}</strong>
|
||||
</span>
|
||||
<span bitDialogContent>
|
||||
{{ "autoConfirmWarning" | i18n }}
|
||||
<a bitLink href="https://bitwarden.com/help/automatic-confirmation/" target="_blank">
|
||||
{{ "autoConfirmWarningLink" | i18n }}
|
||||
<i class="bwi bwi-external-link bwi-fw"></i>
|
||||
</a>
|
||||
</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="button" bitButton buttonType="primary" (click)="dialogRef.close(true)">
|
||||
{{ "turnOn" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="dialogRef.close(false)">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
@ -0,0 +1,19 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
|
||||
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: "./auto-confirm-warning-dialog.component.html",
|
||||
imports: [ButtonModule, DialogModule, CommonModule, I18nPipe],
|
||||
})
|
||||
export class AutoConfirmWarningDialogComponent {
|
||||
constructor(public dialogRef: DialogRef<boolean>) {}
|
||||
|
||||
static open(dialogService: DialogService) {
|
||||
return dialogService.open<boolean>(AutoConfirmWarningDialogComponent);
|
||||
}
|
||||
}
|
||||
1
libs/auto-confirm/src/components/index.ts
Normal file
1
libs/auto-confirm/src/components/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./auto-confirm-warning-dialog.component";
|
||||
@ -0,0 +1,92 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { Router, UrlTree } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs";
|
||||
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
|
||||
import { AutomaticUserConfirmationService } from "../abstractions";
|
||||
|
||||
import { canAccessAutoConfirmSettings } from "./automatic-user-confirmation-settings.guard";
|
||||
|
||||
describe("canAccessAutoConfirmSettings", () => {
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let autoConfirmService: MockProxy<AutomaticUserConfirmationService>;
|
||||
let toastService: MockProxy<ToastService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let router: MockProxy<Router>;
|
||||
|
||||
const mockUserId = newGuid() as UserId;
|
||||
const mockAccount: Account = {
|
||||
id: mockUserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
};
|
||||
let activeAccount$: BehaviorSubject<Account | null>;
|
||||
|
||||
const runGuard = () => {
|
||||
return TestBed.runInInjectionContext(() => {
|
||||
return canAccessAutoConfirmSettings(null as any, null as any) as Observable<
|
||||
boolean | UrlTree
|
||||
>;
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
accountService = mock<AccountService>();
|
||||
autoConfirmService = mock<AutomaticUserConfirmationService>();
|
||||
toastService = mock<ToastService>();
|
||||
i18nService = mock<I18nService>();
|
||||
router = mock<Router>();
|
||||
|
||||
activeAccount$ = new BehaviorSubject<Account | null>(mockAccount);
|
||||
accountService.activeAccount$ = activeAccount$;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: AutomaticUserConfirmationService, useValue: autoConfirmService },
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
{ provide: I18nService, useValue: i18nService },
|
||||
{ provide: Router, useValue: router },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow access when user has permission", async () => {
|
||||
autoConfirmService.canManageAutoConfirm$.mockReturnValue(of(true));
|
||||
|
||||
const result = await firstValueFrom(runGuard());
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should redirect to vault when user lacks permission", async () => {
|
||||
autoConfirmService.canManageAutoConfirm$.mockReturnValue(of(false));
|
||||
const mockUrlTree = {} as UrlTree;
|
||||
router.createUrlTree.mockReturnValue(mockUrlTree);
|
||||
|
||||
const result = await firstValueFrom(runGuard());
|
||||
|
||||
expect(result).toBe(mockUrlTree);
|
||||
expect(router.createUrlTree).toHaveBeenCalledWith(["/tabs/vault"]);
|
||||
});
|
||||
|
||||
it("should not emit when active account is null", async () => {
|
||||
activeAccount$.next(null);
|
||||
autoConfirmService.canManageAutoConfirm$.mockReturnValue(of(true));
|
||||
|
||||
let guardEmitted = false;
|
||||
const subscription = runGuard().subscribe(() => {
|
||||
guardEmitted = true;
|
||||
});
|
||||
|
||||
expect(guardEmitted).toBe(false);
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,35 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { CanActivateFn, Router } from "@angular/router";
|
||||
import { map, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { AutomaticUserConfirmationService } from "../abstractions";
|
||||
|
||||
export const canAccessAutoConfirmSettings: CanActivateFn = () => {
|
||||
const accountService = inject(AccountService);
|
||||
const autoConfirmService = inject(AutomaticUserConfirmationService);
|
||||
const toastService = inject(ToastService);
|
||||
const i18nService = inject(I18nService);
|
||||
const router = inject(Router);
|
||||
|
||||
return accountService.activeAccount$.pipe(
|
||||
filterOutNullish(),
|
||||
switchMap((user) => autoConfirmService.canManageAutoConfirm$(user.id)),
|
||||
map((canManageAutoConfirm) => {
|
||||
if (!canManageAutoConfirm) {
|
||||
toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: i18nService.t("noPermissionsViewPage"),
|
||||
});
|
||||
|
||||
return router.createUrlTree(["/tabs/vault"]);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
};
|
||||
1
libs/auto-confirm/src/guards/index.ts
Normal file
1
libs/auto-confirm/src/guards/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./automatic-user-confirmation-settings.guard";
|
||||
@ -1,3 +1,5 @@
|
||||
export * from "./abstractions";
|
||||
export * from "./components";
|
||||
export * from "./guards";
|
||||
export * from "./models";
|
||||
export * from "./services";
|
||||
@ -1,62 +1,55 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, of, throwError } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
DefaultOrganizationUserService,
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserConfirmRequest,
|
||||
} from "../../organization-user";
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProfileOrganizationResponse } from "@bitwarden/common/admin-console/models/response/profile-organization.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
|
||||
import { AUTO_CONFIRM_STATE, AutoConfirmState } from "../models/auto-confirm-state.model";
|
||||
|
||||
import { DefaultAutomaticUserConfirmationService } from "./default-auto-confirm.service";
|
||||
|
||||
describe("DefaultAutomaticUserConfirmationService", () => {
|
||||
let service: DefaultAutomaticUserConfirmationService;
|
||||
let configService: jest.Mocked<ConfigService>;
|
||||
let apiService: jest.Mocked<ApiService>;
|
||||
let organizationUserService: jest.Mocked<DefaultOrganizationUserService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let organizationUserService: MockProxy<DefaultOrganizationUserService>;
|
||||
let stateProvider: FakeStateProvider;
|
||||
let organizationService: jest.Mocked<InternalOrganizationServiceAbstraction>;
|
||||
let organizationUserApiService: jest.Mocked<OrganizationUserApiService>;
|
||||
let organizationService: MockProxy<InternalOrganizationServiceAbstraction>;
|
||||
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const mockConfirmingUserId = Utils.newGuid() as UserId;
|
||||
const mockOrganizationId = Utils.newGuid() as OrganizationId;
|
||||
const mockUserId = newGuid() as UserId;
|
||||
const mockConfirmingUserId = newGuid() as UserId;
|
||||
const mockOrganizationId = newGuid() as OrganizationId;
|
||||
let mockOrganization: Organization;
|
||||
|
||||
beforeEach(() => {
|
||||
configService = {
|
||||
getFeatureFlag$: jest.fn(),
|
||||
} as any;
|
||||
|
||||
apiService = {
|
||||
getUserPublicKey: jest.fn(),
|
||||
} as any;
|
||||
|
||||
organizationUserService = {
|
||||
buildConfirmRequest: jest.fn(),
|
||||
} as any;
|
||||
|
||||
configService = mock<ConfigService>();
|
||||
apiService = mock<ApiService>();
|
||||
organizationUserService = mock<DefaultOrganizationUserService>();
|
||||
stateProvider = new FakeStateProvider(mockAccountServiceWith(mockUserId));
|
||||
|
||||
organizationService = {
|
||||
organizations$: jest.fn(),
|
||||
} as any;
|
||||
|
||||
organizationUserApiService = {
|
||||
postOrganizationUserConfirm: jest.fn(),
|
||||
} as any;
|
||||
organizationService = mock<InternalOrganizationServiceAbstraction>();
|
||||
organizationUserApiService = mock<OrganizationUserApiService>();
|
||||
policyService = mock<PolicyService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@ -70,6 +63,7 @@ describe("DefaultAutomaticUserConfirmationService", () => {
|
||||
useValue: organizationService,
|
||||
},
|
||||
{ provide: OrganizationUserApiService, useValue: organizationUserApiService },
|
||||
{ provide: PolicyService, useValue: policyService },
|
||||
],
|
||||
});
|
||||
|
||||
@ -80,9 +74,13 @@ describe("DefaultAutomaticUserConfirmationService", () => {
|
||||
stateProvider,
|
||||
organizationService,
|
||||
organizationUserApiService,
|
||||
policyService,
|
||||
);
|
||||
|
||||
const mockOrgData = new OrganizationData({} as any, {} as any);
|
||||
const mockOrgData = new OrganizationData({} as ProfileOrganizationResponse, {
|
||||
isMember: true,
|
||||
isProviderUser: false,
|
||||
});
|
||||
mockOrgData.id = mockOrganizationId;
|
||||
mockOrgData.useAutomaticUserConfirmation = true;
|
||||
|
||||
@ -180,7 +178,7 @@ describe("DefaultAutomaticUserConfirmationService", () => {
|
||||
});
|
||||
|
||||
it("should preserve other user configurations when updating", async () => {
|
||||
const otherUserId = Utils.newGuid() as UserId;
|
||||
const otherUserId = newGuid() as UserId;
|
||||
const otherConfig = new AutoConfirmState();
|
||||
otherConfig.enabled = true;
|
||||
|
||||
@ -209,12 +207,13 @@ describe("DefaultAutomaticUserConfirmationService", () => {
|
||||
beforeEach(() => {
|
||||
const organizations$ = new BehaviorSubject<Organization[]>([mockOrganization]);
|
||||
organizationService.organizations$.mockReturnValue(organizations$);
|
||||
policyService.policyAppliesToUser$.mockReturnValue(of(true));
|
||||
});
|
||||
|
||||
it("should return true when feature flag is enabled and organization allows management", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId, mockOrganizationId);
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId);
|
||||
const canManage = await firstValueFrom(canManage$);
|
||||
|
||||
expect(canManage).toBe(true);
|
||||
@ -223,7 +222,7 @@ describe("DefaultAutomaticUserConfirmationService", () => {
|
||||
it("should return false when feature flag is disabled", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId, mockOrganizationId);
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId);
|
||||
const canManage = await firstValueFrom(canManage$);
|
||||
|
||||
expect(canManage).toBe(false);
|
||||
@ -233,7 +232,10 @@ describe("DefaultAutomaticUserConfirmationService", () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
// Create organization without manageUsers permission
|
||||
const mockOrgData = new OrganizationData({} as any, {} as any);
|
||||
const mockOrgData = new OrganizationData({} as ProfileOrganizationResponse, {
|
||||
isMember: true,
|
||||
isProviderUser: false,
|
||||
});
|
||||
mockOrgData.id = mockOrganizationId;
|
||||
mockOrgData.useAutomaticUserConfirmation = true;
|
||||
const permissions = new PermissionsApi();
|
||||
@ -244,7 +246,7 @@ describe("DefaultAutomaticUserConfirmationService", () => {
|
||||
const organizations$ = new BehaviorSubject<Organization[]>([orgWithoutManageUsers]);
|
||||
organizationService.organizations$.mockReturnValue(organizations$);
|
||||
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId, mockOrganizationId);
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId);
|
||||
const canManage = await firstValueFrom(canManage$);
|
||||
|
||||
expect(canManage).toBe(false);
|
||||
@ -254,7 +256,10 @@ describe("DefaultAutomaticUserConfirmationService", () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
// Create organization without useAutomaticUserConfirmation
|
||||
const mockOrgData = new OrganizationData({} as any, {} as any);
|
||||
const mockOrgData = new OrganizationData({} as ProfileOrganizationResponse, {
|
||||
isMember: true,
|
||||
isProviderUser: false,
|
||||
});
|
||||
mockOrgData.id = mockOrganizationId;
|
||||
mockOrgData.useAutomaticUserConfirmation = false;
|
||||
const permissions = new PermissionsApi();
|
||||
@ -265,7 +270,7 @@ describe("DefaultAutomaticUserConfirmationService", () => {
|
||||
const organizations$ = new BehaviorSubject<Organization[]>([orgWithoutAutoConfirm]);
|
||||
organizationService.organizations$.mockReturnValue(organizations$);
|
||||
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId, mockOrganizationId);
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId);
|
||||
const canManage = await firstValueFrom(canManage$);
|
||||
|
||||
expect(canManage).toBe(false);
|
||||
@ -277,7 +282,31 @@ describe("DefaultAutomaticUserConfirmationService", () => {
|
||||
const organizations$ = new BehaviorSubject<Organization[]>([]);
|
||||
organizationService.organizations$.mockReturnValue(organizations$);
|
||||
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId, mockOrganizationId);
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId);
|
||||
const canManage = await firstValueFrom(canManage$);
|
||||
|
||||
expect(canManage).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when the user is not a member of any organizations", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
// Create organization where user is not a member
|
||||
const mockOrgData = new OrganizationData({} as ProfileOrganizationResponse, {
|
||||
isMember: false,
|
||||
isProviderUser: false,
|
||||
});
|
||||
mockOrgData.id = mockOrganizationId;
|
||||
mockOrgData.useAutomaticUserConfirmation = true;
|
||||
const permissions = new PermissionsApi();
|
||||
permissions.manageUsers = true;
|
||||
mockOrgData.permissions = permissions;
|
||||
const orgWhereNotMember = new Organization(mockOrgData);
|
||||
|
||||
const organizations$ = new BehaviorSubject<Organization[]>([orgWhereNotMember]);
|
||||
organizationService.organizations$.mockReturnValue(organizations$);
|
||||
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId);
|
||||
const canManage = await firstValueFrom(canManage$);
|
||||
|
||||
expect(canManage).toBe(false);
|
||||
@ -286,11 +315,58 @@ describe("DefaultAutomaticUserConfirmationService", () => {
|
||||
it("should use the correct feature flag", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId, mockOrganizationId);
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId);
|
||||
await firstValueFrom(canManage$);
|
||||
|
||||
expect(configService.getFeatureFlag$).toHaveBeenCalledWith(FeatureFlag.AutoConfirm);
|
||||
});
|
||||
|
||||
it("should return false when policy does not apply to user", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
policyService.policyAppliesToUser$.mockReturnValue(of(false));
|
||||
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId);
|
||||
const canManage = await firstValueFrom(canManage$);
|
||||
|
||||
expect(canManage).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true when policy applies to user", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
policyService.policyAppliesToUser$.mockReturnValue(of(true));
|
||||
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId);
|
||||
const canManage = await firstValueFrom(canManage$);
|
||||
|
||||
expect(canManage).toBe(true);
|
||||
});
|
||||
|
||||
it("should check policy with correct PolicyType and userId", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
policyService.policyAppliesToUser$.mockReturnValue(of(true));
|
||||
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId);
|
||||
await firstValueFrom(canManage$);
|
||||
|
||||
expect(policyService.policyAppliesToUser$).toHaveBeenCalledWith(
|
||||
PolicyType.AutoConfirm,
|
||||
mockUserId,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return false when feature flag is enabled but policy does not apply", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
policyService.policyAppliesToUser$.mockReturnValue(of(false));
|
||||
|
||||
const canManage$ = service.canManageAutoConfirm$(mockUserId);
|
||||
const canManage = await firstValueFrom(canManage$);
|
||||
|
||||
expect(canManage).toBe(false);
|
||||
expect(policyService.policyAppliesToUser$).toHaveBeenCalledWith(
|
||||
PolicyType.AutoConfirm,
|
||||
mockUserId,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("autoConfirmUser", () => {
|
||||
@ -305,8 +381,11 @@ describe("DefaultAutomaticUserConfirmationService", () => {
|
||||
const organizations$ = new BehaviorSubject<Organization[]>([mockOrganization]);
|
||||
organizationService.organizations$.mockReturnValue(organizations$);
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
policyService.policyAppliesToUser$.mockReturnValue(of(true));
|
||||
|
||||
apiService.getUserPublicKey.mockResolvedValue({ publicKey: mockPublicKey } as any);
|
||||
apiService.getUserPublicKey.mockResolvedValue({
|
||||
publicKey: mockPublicKey,
|
||||
} as UserKeyResponse);
|
||||
jest.spyOn(Utils, "fromB64ToArray").mockReturnValue(mockPublicKeyArray);
|
||||
organizationUserService.buildConfirmRequest.mockReturnValue(of(mockConfirmRequest));
|
||||
organizationUserApiService.postOrganizationUserConfirm.mockResolvedValue(undefined);
|
||||
@ -1,17 +1,20 @@
|
||||
import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { StateProvider } from "@bitwarden/state";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { OrganizationUserApiService, OrganizationUserService } from "../../organization-user";
|
||||
import { AutomaticUserConfirmationService } from "../abstractions/auto-confirm.service.abstraction";
|
||||
import { AUTO_CONFIRM_STATE, AutoConfirmState } from "../models/auto-confirm-state.model";
|
||||
|
||||
@ -23,6 +26,7 @@ export class DefaultAutomaticUserConfirmationService implements AutomaticUserCon
|
||||
private stateProvider: StateProvider,
|
||||
private organizationService: InternalOrganizationServiceAbstraction,
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
private policyService: PolicyService,
|
||||
) {}
|
||||
private autoConfirmState(userId: UserId) {
|
||||
return this.stateProvider.getUser(userId, AUTO_CONFIRM_STATE);
|
||||
@ -43,15 +47,19 @@ export class DefaultAutomaticUserConfirmationService implements AutomaticUserCon
|
||||
});
|
||||
}
|
||||
|
||||
canManageAutoConfirm$(userId: UserId, organizationId: OrganizationId): Observable<boolean> {
|
||||
canManageAutoConfirm$(userId: UserId): Observable<boolean> {
|
||||
return combineLatest([
|
||||
this.configService.getFeatureFlag$(FeatureFlag.AutoConfirm),
|
||||
this.organizationService.organizations$(userId).pipe(getById(organizationId)),
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
// auto-confirm does not allow the user to be part of any other organization (even if admin or owner)
|
||||
// so we can assume that the first organization is the relevant one to test.
|
||||
.pipe(map((organizations) => organizations[0])),
|
||||
this.policyService.policyAppliesToUser$(PolicyType.AutoConfirm, userId),
|
||||
]).pipe(
|
||||
map(
|
||||
([enabled, organization]) =>
|
||||
(enabled && organization?.canManageUsers && organization?.useAutomaticUserConfirmation) ??
|
||||
false,
|
||||
([enabled, organization, policyEnabled]) =>
|
||||
enabled && policyEnabled && (organization?.canManageAutoConfirm ?? false),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -62,7 +70,7 @@ export class DefaultAutomaticUserConfirmationService implements AutomaticUserCon
|
||||
organization: Organization,
|
||||
): Promise<void> {
|
||||
await firstValueFrom(
|
||||
this.canManageAutoConfirm$(userId, organization.id).pipe(
|
||||
this.canManageAutoConfirm$(userId).pipe(
|
||||
map((canManage) => {
|
||||
if (!canManage) {
|
||||
throw new Error("Cannot automatically confirm user (insufficient permissions)");
|
||||
28
libs/auto-confirm/test.setup.ts
Normal file
28
libs/auto-confirm/test.setup.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { webcrypto } from "crypto";
|
||||
import "@bitwarden/ui-common/setup-jest";
|
||||
|
||||
Object.defineProperty(window, "CSS", { value: null });
|
||||
Object.defineProperty(window, "getComputedStyle", {
|
||||
value: () => {
|
||||
return {
|
||||
display: "none",
|
||||
appearance: ["-webkit-appearance"],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(document, "doctype", {
|
||||
value: "<!DOCTYPE html>",
|
||||
});
|
||||
Object.defineProperty(document.body.style, "transform", {
|
||||
value: () => {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(window, "crypto", {
|
||||
value: webcrypto,
|
||||
});
|
||||
6
libs/auto-confirm/tsconfig.eslint.json
Normal file
6
libs/auto-confirm/tsconfig.eslint.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": ["src/**/*.ts", "src/**/*.js"],
|
||||
"exclude": ["**/build", "**/dist"]
|
||||
}
|
||||
13
libs/auto-confirm/tsconfig.json
Normal file
13
libs/auto-confirm/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
libs/auto-confirm/tsconfig.lib.json
Normal file
10
libs/auto-confirm/tsconfig.lib.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["jest.config.js", "src/**/*.spec.ts"]
|
||||
}
|
||||
10
libs/auto-confirm/tsconfig.spec.json
Normal file
10
libs/auto-confirm/tsconfig.spec.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node10",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
|
||||
}
|
||||
@ -75,8 +75,8 @@ export function canAccessEmergencyAccess(
|
||||
) {
|
||||
return combineLatest([
|
||||
configService.getFeatureFlag$(FeatureFlag.AutoConfirm),
|
||||
policyService.policiesByType$(PolicyType.AutoConfirm, userId),
|
||||
]).pipe(map(([enabled, policies]) => !enabled || !policies.some((p) => p.enabled)));
|
||||
policyService.policyAppliesToUser$(PolicyType.AutoConfirm, userId),
|
||||
]).pipe(map(([enabled, policyAppliesToUser]) => !(enabled && policyAppliesToUser)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -381,6 +381,13 @@ export class Organization {
|
||||
return this.familySponsorshipAvailable || this.familySponsorshipFriendlyName !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not call this function to perform business logic, consider using the function in @link AutomaticUserConfirmationService instead.
|
||||
**/
|
||||
get canManageAutoConfirm() {
|
||||
return this.isMember && this.canManageUsers && this.useAutomaticUserConfirmation;
|
||||
}
|
||||
|
||||
static fromJSON(json: Jsonify<Organization>) {
|
||||
if (json == null) {
|
||||
return null;
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@ -314,6 +314,11 @@
|
||||
"version": "0.0.0",
|
||||
"license": "GPL-3.0"
|
||||
},
|
||||
"libs/auto-confirm": {
|
||||
"name": "@bitwarden/auto-confirm",
|
||||
"version": "0.0.1",
|
||||
"license": "GPL-3.0"
|
||||
},
|
||||
"libs/billing": {
|
||||
"name": "@bitwarden/billing",
|
||||
"version": "0.0.0",
|
||||
@ -4598,6 +4603,10 @@
|
||||
"resolved": "libs/auth",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@bitwarden/auto-confirm": {
|
||||
"resolved": "libs/auto-confirm",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@bitwarden/billing": {
|
||||
"resolved": "libs/billing",
|
||||
"link": true
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
"@bitwarden/assets/svg": ["libs/assets/src/svg/index.ts"],
|
||||
"@bitwarden/auth/angular": ["./libs/auth/src/angular"],
|
||||
"@bitwarden/auth/common": ["./libs/auth/src/common"],
|
||||
"@bitwarden/auto-confirm": ["libs/auto-confirm/src/index.ts"],
|
||||
"@bitwarden/billing": ["./libs/billing/src"],
|
||||
"@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"],
|
||||
"@bitwarden/browser/*": ["./apps/browser/src/*"],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user