mirror of
https://github.com/bitwarden/browser
synced 2026-02-27 01:53:23 +00:00
Merge branch 'main' into PM-30922-Client-changes-to-encrypt-send-access-email-list
This commit is contained in:
@@ -25,7 +25,6 @@
|
||||
<div
|
||||
class="tw-size-full tw-styled-scrollbar"
|
||||
data-testid="popup-layout-scroll-region"
|
||||
(scroll)="handleScroll($event)"
|
||||
[ngClass]="{
|
||||
'!tw-overflow-hidden': hideOverflow(),
|
||||
'tw-overflow-y-auto': !hideOverflow(),
|
||||
|
||||
@@ -3,13 +3,17 @@ import {
|
||||
booleanAttribute,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
ElementRef,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { filter, switchMap, fromEvent, startWith, map } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ScrollLayoutHostDirective } from "@bitwarden/components";
|
||||
import { ScrollLayoutHostDirective, ScrollLayoutService } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "popup-page",
|
||||
@@ -22,6 +26,8 @@ import { ScrollLayoutHostDirective } from "@bitwarden/components";
|
||||
})
|
||||
export class PopupPageComponent {
|
||||
protected i18nService = inject(I18nService);
|
||||
private scrollLayout = inject(ScrollLayoutService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
readonly loading = input<boolean>(false);
|
||||
|
||||
@@ -33,10 +39,21 @@ export class PopupPageComponent {
|
||||
protected readonly scrolled = signal(false);
|
||||
isScrolled = this.scrolled.asReadonly();
|
||||
|
||||
constructor() {
|
||||
this.scrollLayout.scrollableRef$
|
||||
.pipe(
|
||||
filter((ref): ref is ElementRef<HTMLElement> => ref != null),
|
||||
switchMap((ref) =>
|
||||
fromEvent(ref.nativeElement, "scroll").pipe(
|
||||
startWith(null),
|
||||
map(() => ref.nativeElement.scrollTop !== 0),
|
||||
),
|
||||
),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((isScrolled) => this.scrolled.set(isScrolled));
|
||||
}
|
||||
|
||||
/** Accessible loading label for the spinner. Defaults to "loading" */
|
||||
readonly loadingText = input<string | undefined>(this.i18nService.t("loading"));
|
||||
|
||||
handleScroll(event: Event) {
|
||||
this.scrolled.set((event.currentTarget as HTMLElement).scrollTop !== 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export class PopupSizeService {
|
||||
/** Begin listening for state changes */
|
||||
async init() {
|
||||
this.width$.subscribe((width: PopupWidthOption) => {
|
||||
PopupSizeService.setStyle(width);
|
||||
void PopupSizeService.setStyle(width);
|
||||
localStorage.setItem(PopupSizeService.LocalStorageKey, width);
|
||||
});
|
||||
}
|
||||
@@ -77,8 +77,9 @@ export class PopupSizeService {
|
||||
}
|
||||
}
|
||||
|
||||
private static setStyle(width: PopupWidthOption) {
|
||||
if (!BrowserPopupUtils.inPopup(window)) {
|
||||
private static async setStyle(width: PopupWidthOption) {
|
||||
const isInTab = await BrowserPopupUtils.isInTab();
|
||||
if (!BrowserPopupUtils.inPopup(window) || isInTab) {
|
||||
return;
|
||||
}
|
||||
const pxWidth = PopupWidthOptions[width] ?? PopupWidthOptions.default;
|
||||
@@ -91,6 +92,6 @@ export class PopupSizeService {
|
||||
**/
|
||||
static initBodyWidthFromLocalStorage() {
|
||||
const storedValue = localStorage.getItem(PopupSizeService.LocalStorageKey);
|
||||
this.setStyle(storedValue as any);
|
||||
void this.setStyle(storedValue as any);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,12 @@ import {
|
||||
WINDOW,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { AUTOFILL_NUDGE_SERVICE } from "@bitwarden/angular/vault";
|
||||
import { SingleNudgeService } from "@bitwarden/angular/vault/services/default-single-nudge.service";
|
||||
import {
|
||||
AUTOFILL_NUDGE_SERVICE,
|
||||
AUTO_CONFIRM_NUDGE_SERVICE,
|
||||
AutoConfirmNudgeService,
|
||||
} from "@bitwarden/angular/vault";
|
||||
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
|
||||
import {
|
||||
LoginComponentService,
|
||||
TwoFactorAuthComponentService,
|
||||
@@ -786,9 +790,14 @@ const safeProviders: SafeProvider[] = [
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AUTOFILL_NUDGE_SERVICE as SafeInjectionToken<SingleNudgeService>,
|
||||
provide: AUTOFILL_NUDGE_SERVICE as SafeInjectionToken<BrowserAutofillNudgeService>,
|
||||
useClass: BrowserAutofillNudgeService,
|
||||
deps: [],
|
||||
deps: [StateProvider, VaultProfileService, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AUTO_CONFIRM_NUDGE_SERVICE as SafeInjectionToken<AutoConfirmNudgeService>,
|
||||
useClass: AutoConfirmNudgeService,
|
||||
deps: [StateProvider, AutomaticUserConfirmationService],
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CdkVirtualScrollableElement } from "@angular/cdk/scrolling";
|
||||
import { ChangeDetectionStrategy, Component, input, NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
import { TestBed, fakeAsync, flush, tick } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
@@ -394,21 +393,28 @@ describe("VaultV2Component", () => {
|
||||
expect(values[values.length - 1]).toBe(false);
|
||||
});
|
||||
|
||||
it("ngAfterViewInit waits for allFilters$ then starts scroll position service", fakeAsync(() => {
|
||||
it("passes popup-page scroll region element to scroll position service", fakeAsync(() => {
|
||||
const fixture = TestBed.createComponent(VaultV2Component);
|
||||
const component = fixture.componentInstance;
|
||||
|
||||
const readySubject$ = component["readySubject"] as unknown as BehaviorSubject<boolean>;
|
||||
const itemsLoading$ = itemsSvc.loading$ as unknown as BehaviorSubject<boolean>;
|
||||
const allFilters$ = filtersSvc.allFilters$ as unknown as Subject<any>;
|
||||
|
||||
(component as any).virtualScrollElement = {} as CdkVirtualScrollableElement;
|
||||
|
||||
component.ngAfterViewInit();
|
||||
expect(scrollSvc.start).not.toHaveBeenCalled();
|
||||
|
||||
allFilters$.next({ any: true });
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(scrollSvc.start).toHaveBeenCalledTimes(1);
|
||||
expect(scrollSvc.start).toHaveBeenCalledWith((component as any).virtualScrollElement);
|
||||
const scrollRegion = fixture.nativeElement.querySelector(
|
||||
'[data-testid="popup-layout-scroll-region"]',
|
||||
) as HTMLElement;
|
||||
|
||||
flush();
|
||||
// Unblock loading
|
||||
itemsLoading$.next(false);
|
||||
readySubject$.next(true);
|
||||
allFilters$.next({});
|
||||
tick();
|
||||
|
||||
expect(scrollSvc.start).toHaveBeenCalledWith(scrollRegion);
|
||||
}));
|
||||
|
||||
it("showPremiumDialog opens PremiumUpgradeDialogComponent", () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||
import { CdkVirtualScrollableElement, ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { AfterViewInit, Component, DestroyRef, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { Component, DestroyRef, effect, inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import {
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
ButtonModule,
|
||||
DialogService,
|
||||
NoItemsModule,
|
||||
ScrollLayoutService,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
@@ -119,11 +120,7 @@ type VaultState = UnionOfValues<typeof VaultState>;
|
||||
],
|
||||
providers: [{ provide: VaultItemsTransferService, useClass: DefaultVaultItemsTransferService }],
|
||||
})
|
||||
export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement;
|
||||
|
||||
export class VaultV2Component implements OnInit, OnDestroy {
|
||||
NudgeType = NudgeType;
|
||||
cipherType = CipherType;
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
@@ -308,16 +305,21 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (this.virtualScrollElement) {
|
||||
// The filters component can cause the size of the virtual scroll element to change,
|
||||
// which can cause the scroll position to be land in the wrong spot. To fix this,
|
||||
// wait until all filters are populated before restoring the scroll position.
|
||||
this.allFilters$.pipe(take(1), takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
this.vaultScrollPositionService.start(this.virtualScrollElement!);
|
||||
private readonly scrollLayout = inject(ScrollLayoutService);
|
||||
|
||||
private readonly _scrollPositionEffect = effect((onCleanup) => {
|
||||
const sub = combineLatest([this.scrollLayout.scrollableRef$, this.allFilters$, this.loading$])
|
||||
.pipe(
|
||||
filter(([ref, _filters, loading]) => !!ref && !loading),
|
||||
take(1),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe(([ref]) => {
|
||||
this.vaultScrollPositionService.start(ref!.nativeElement);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onCleanup(() => sub.unsubscribe());
|
||||
});
|
||||
|
||||
async ngOnInit() {
|
||||
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CdkVirtualScrollableElement } from "@angular/cdk/scrolling";
|
||||
import { fakeAsync, TestBed, tick } from "@angular/core/testing";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import { Subject, Subscription } from "rxjs";
|
||||
@@ -66,21 +65,18 @@ describe("VaultPopupScrollPositionService", () => {
|
||||
});
|
||||
|
||||
describe("start", () => {
|
||||
const elementScrolled$ = new Subject();
|
||||
const focus = jest.fn();
|
||||
const nativeElement = {
|
||||
scrollTop: 0,
|
||||
querySelector: jest.fn(() => ({ focus })),
|
||||
addEventListener: jest.fn(),
|
||||
style: {
|
||||
visibility: "",
|
||||
},
|
||||
};
|
||||
const virtualElement = {
|
||||
elementScrolled: () => elementScrolled$,
|
||||
getElementRef: () => ({ nativeElement }),
|
||||
scrollTo: jest.fn(),
|
||||
} as unknown as CdkVirtualScrollableElement;
|
||||
let scrollElement: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
scrollElement = document.createElement("div");
|
||||
|
||||
(scrollElement as any).scrollTo = jest.fn(function scrollTo(opts: { top?: number }) {
|
||||
if (opts?.top != null) {
|
||||
(scrollElement as any).scrollTop = opts.top;
|
||||
}
|
||||
});
|
||||
(scrollElement as any).scrollTop = 0;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// remove the actual subscription created by `.subscribe`
|
||||
@@ -89,47 +85,55 @@ describe("VaultPopupScrollPositionService", () => {
|
||||
|
||||
describe("initial scroll position", () => {
|
||||
beforeEach(() => {
|
||||
(virtualElement.scrollTo as jest.Mock).mockClear();
|
||||
nativeElement.querySelector.mockClear();
|
||||
((scrollElement as any).scrollTo as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
it("does not scroll when `scrollPosition` is null", () => {
|
||||
service["scrollPosition"] = null;
|
||||
|
||||
service.start(virtualElement);
|
||||
service.start(scrollElement);
|
||||
|
||||
expect(virtualElement.scrollTo).not.toHaveBeenCalled();
|
||||
expect((scrollElement as any).scrollTo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("scrolls the virtual element to `scrollPosition`", fakeAsync(() => {
|
||||
it("scrolls the element to `scrollPosition` (async via setTimeout)", fakeAsync(() => {
|
||||
service["scrollPosition"] = 500;
|
||||
nativeElement.scrollTop = 500;
|
||||
|
||||
service.start(virtualElement);
|
||||
service.start(scrollElement);
|
||||
tick();
|
||||
|
||||
expect(virtualElement.scrollTo).toHaveBeenCalledWith({ behavior: "instant", top: 500 });
|
||||
expect((scrollElement as any).scrollTo).toHaveBeenCalledWith({
|
||||
behavior: "instant",
|
||||
top: 500,
|
||||
});
|
||||
expect((scrollElement as any).scrollTop).toBe(500);
|
||||
}));
|
||||
});
|
||||
|
||||
describe("scroll listener", () => {
|
||||
it("unsubscribes from any existing subscription", () => {
|
||||
service.start(virtualElement);
|
||||
service.start(scrollElement);
|
||||
|
||||
expect(unsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("subscribes to `elementScrolled`", fakeAsync(() => {
|
||||
virtualElement.measureScrollOffset = jest.fn(() => 455);
|
||||
it("stores scrollTop on subsequent scroll events (skips first)", fakeAsync(() => {
|
||||
service["scrollPosition"] = null;
|
||||
|
||||
service.start(virtualElement);
|
||||
service.start(scrollElement);
|
||||
|
||||
elementScrolled$.next(null); // first subscription is skipped by `skip(1)`
|
||||
elementScrolled$.next(null);
|
||||
// First scroll event is intentionally ignored (equivalent to old skip(1)).
|
||||
(scrollElement as any).scrollTop = 111;
|
||||
scrollElement.dispatchEvent(new Event("scroll"));
|
||||
tick();
|
||||
|
||||
expect(service["scrollPosition"]).toBeNull();
|
||||
|
||||
// Second scroll event should persist.
|
||||
(scrollElement as any).scrollTop = 455;
|
||||
scrollElement.dispatchEvent(new Event("scroll"));
|
||||
tick();
|
||||
|
||||
expect(virtualElement.measureScrollOffset).toHaveBeenCalledTimes(1);
|
||||
expect(virtualElement.measureScrollOffset).toHaveBeenCalledWith("top");
|
||||
expect(service["scrollPosition"]).toBe(455);
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { CdkVirtualScrollableElement } from "@angular/cdk/scrolling";
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import { filter, skip, Subscription } from "rxjs";
|
||||
import { filter, fromEvent, Subscription } from "rxjs";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
@@ -31,24 +30,25 @@ export class VaultPopupScrollPositionService {
|
||||
}
|
||||
|
||||
/** Scrolls the user to the stored scroll position and starts tracking scroll of the page. */
|
||||
start(virtualScrollElement: CdkVirtualScrollableElement) {
|
||||
start(scrollElement: HTMLElement) {
|
||||
if (this.hasScrollPosition()) {
|
||||
// Use `setTimeout` to scroll after rendering is complete
|
||||
setTimeout(() => {
|
||||
virtualScrollElement.scrollTo({ top: this.scrollPosition!, behavior: "instant" });
|
||||
scrollElement.scrollTo({ top: this.scrollPosition!, behavior: "instant" });
|
||||
});
|
||||
}
|
||||
|
||||
this.scrollSubscription?.unsubscribe();
|
||||
|
||||
// Skip the first scroll event to avoid settings the scroll from the above `scrollTo` call
|
||||
this.scrollSubscription = virtualScrollElement
|
||||
?.elementScrolled()
|
||||
.pipe(skip(1))
|
||||
.subscribe(() => {
|
||||
const offset = virtualScrollElement.measureScrollOffset("top");
|
||||
this.scrollPosition = offset;
|
||||
});
|
||||
let skipped = false;
|
||||
this.scrollSubscription = fromEvent(scrollElement, "scroll").subscribe(() => {
|
||||
if (!skipped) {
|
||||
skipped = true;
|
||||
return;
|
||||
}
|
||||
this.scrollPosition = scrollElement.scrollTop;
|
||||
});
|
||||
}
|
||||
|
||||
/** Stops the scroll listener from updating the stored location. */
|
||||
|
||||
@@ -19,5 +19,14 @@ windows-core = { workspace = true }
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dev-dependencies]
|
||||
windows = { workspace = true, features = [
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_Foundation",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_Graphics_Gdi",
|
||||
] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
324
apps/desktop/desktop_native/autotype/tests/integration_tests.rs
Normal file
324
apps/desktop/desktop_native/autotype/tests/integration_tests.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use autotype::{get_foreground_window_title, type_input};
|
||||
use serial_test::serial;
|
||||
use tracing::debug;
|
||||
use windows::Win32::{
|
||||
Foundation::{COLORREF, HINSTANCE, HMODULE, HWND, LPARAM, LRESULT, WPARAM},
|
||||
Graphics::Gdi::{CreateSolidBrush, UpdateWindow, ValidateRect, COLOR_WINDOW},
|
||||
System::LibraryLoader::{GetModuleHandleA, GetModuleHandleW},
|
||||
UI::WindowsAndMessaging::*,
|
||||
};
|
||||
use windows_core::{s, w, Result, PCSTR, PCWSTR};
|
||||
|
||||
struct TestWindow {
|
||||
handle: HWND,
|
||||
capture: Option<InputCapture>,
|
||||
}
|
||||
|
||||
impl Drop for TestWindow {
|
||||
fn drop(&mut self) {
|
||||
// Clean up the InputCapture pointer
|
||||
unsafe {
|
||||
let capture_ptr = GetWindowLongPtrW(self.handle, GWLP_USERDATA) as *mut InputCapture;
|
||||
if !capture_ptr.is_null() {
|
||||
let _ = Box::from_raw(capture_ptr);
|
||||
}
|
||||
CloseWindow(self.handle).expect("window handle should be closeable");
|
||||
DestroyWindow(self.handle).expect("window handle should be destroyable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// state to capture keyboard input
|
||||
#[derive(Clone)]
|
||||
struct InputCapture {
|
||||
chars: Arc<Mutex<Vec<char>>>,
|
||||
}
|
||||
|
||||
impl InputCapture {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
chars: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_chars(&self) -> Vec<char> {
|
||||
self.chars
|
||||
.lock()
|
||||
.expect("mutex should not be poisoned")
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// Custom window procedure that captures input
|
||||
unsafe extern "system" fn capture_input_proc(
|
||||
handle: HWND,
|
||||
msg: u32,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
match msg {
|
||||
WM_CREATE => {
|
||||
// Store the InputCapture pointer in window data
|
||||
let create_struct = lparam.0 as *const CREATESTRUCTW;
|
||||
let capture_ptr = (*create_struct).lpCreateParams as *mut InputCapture;
|
||||
SetWindowLongPtrW(handle, GWLP_USERDATA, capture_ptr as isize);
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_CHAR => {
|
||||
// Get the InputCapture from window data
|
||||
let capture_ptr = GetWindowLongPtrW(handle, GWLP_USERDATA) as *mut InputCapture;
|
||||
if !capture_ptr.is_null() {
|
||||
let capture = &*capture_ptr;
|
||||
if let Some(ch) = char::from_u32(wparam.0 as u32) {
|
||||
capture
|
||||
.chars
|
||||
.lock()
|
||||
.expect("mutex should not be poisoned")
|
||||
.push(ch);
|
||||
}
|
||||
}
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(handle, msg, wparam, lparam),
|
||||
}
|
||||
}
|
||||
|
||||
// A pointer to the window procedure
|
||||
type ProcType = unsafe extern "system" fn(HWND, u32, WPARAM, LPARAM) -> LRESULT;
|
||||
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-wndproc>
|
||||
extern "system" fn show_window_proc(
|
||||
handle: HWND, // the window handle
|
||||
message: u32, // the system message
|
||||
wparam: WPARAM, /* additional message information. The contents of the wParam parameter
|
||||
* depend on the value of the message parameter. */
|
||||
lparam: LPARAM, /* additional message information. The contents of the lParam parameter
|
||||
* depend on the value of the message parameter. */
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match message {
|
||||
WM_PAINT => {
|
||||
debug!("WM_PAINT");
|
||||
let res = ValidateRect(Some(handle), None);
|
||||
debug_assert!(res.ok().is_ok());
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
debug!("WM_DESTROY");
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcA(handle, message, wparam, lparam),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestWindow {
|
||||
fn set_foreground(&self) -> Result<()> {
|
||||
unsafe {
|
||||
let _ = ShowWindow(self.handle, SW_SHOW);
|
||||
let _ = SetForegroundWindow(self.handle);
|
||||
let _ = UpdateWindow(self.handle);
|
||||
let _ = SetForegroundWindow(self.handle);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wait_for_input(&self, timeout_ms: u64) {
|
||||
let start = std::time::Instant::now();
|
||||
while start.elapsed().as_millis() < timeout_ms as u128 {
|
||||
process_messages();
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_messages() {
|
||||
unsafe {
|
||||
let mut msg = MSG::default();
|
||||
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_input_window(title: PCWSTR, proc_type: ProcType) -> Result<TestWindow> {
|
||||
unsafe {
|
||||
let instance = GetModuleHandleW(None).unwrap_or(HMODULE(std::ptr::null_mut()));
|
||||
let instance: HINSTANCE = instance.into();
|
||||
debug_assert!(!instance.is_invalid());
|
||||
|
||||
let window_class = w!("show_window");
|
||||
|
||||
// Register window class with our custom proc
|
||||
let wc = WNDCLASSW {
|
||||
lpfnWndProc: Some(proc_type),
|
||||
hInstance: instance,
|
||||
lpszClassName: window_class,
|
||||
hbrBackground: CreateSolidBrush(COLORREF(
|
||||
(COLOR_WINDOW.0 + 1).try_into().expect("i32 to fit in u32"),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _atom = RegisterClassW(&wc);
|
||||
|
||||
let capture = InputCapture::new();
|
||||
|
||||
// Pass InputCapture as lpParam
|
||||
let capture_ptr = Box::into_raw(Box::new(capture.clone()));
|
||||
|
||||
// Create window
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/learnwin32/creating-a-window>
|
||||
let handle = CreateWindowExW(
|
||||
WINDOW_EX_STYLE(0),
|
||||
window_class,
|
||||
title,
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
400,
|
||||
300,
|
||||
None,
|
||||
None,
|
||||
Some(instance),
|
||||
Some(capture_ptr as *const _),
|
||||
)
|
||||
.expect("window should be created");
|
||||
|
||||
// Process pending messages
|
||||
process_messages();
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
Ok(TestWindow {
|
||||
handle,
|
||||
capture: Some(capture),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn create_title_window(title: PCSTR, proc_type: ProcType) -> Result<TestWindow> {
|
||||
unsafe {
|
||||
let instance = GetModuleHandleA(None)?;
|
||||
let instance: HINSTANCE = instance.into();
|
||||
debug_assert!(!instance.is_invalid());
|
||||
|
||||
let window_class = s!("input_window");
|
||||
|
||||
// Register window class with our custom proc
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassa>
|
||||
let wc = WNDCLASSA {
|
||||
hCursor: LoadCursorW(None, IDC_ARROW)?,
|
||||
hInstance: instance,
|
||||
lpszClassName: window_class,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(proc_type),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _atom = RegisterClassA(&wc);
|
||||
|
||||
// Create window
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/learnwin32/creating-a-window>
|
||||
let handle = CreateWindowExA(
|
||||
WINDOW_EX_STYLE::default(),
|
||||
window_class,
|
||||
title,
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
800,
|
||||
600,
|
||||
None,
|
||||
None,
|
||||
Some(instance),
|
||||
None,
|
||||
)
|
||||
.expect("window should be created");
|
||||
|
||||
Ok(TestWindow {
|
||||
handle,
|
||||
capture: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn test_get_active_window_title_success() {
|
||||
let title;
|
||||
{
|
||||
let window = create_title_window(s!("TITLE_FOOBAR"), show_window_proc).unwrap();
|
||||
window.set_foreground().unwrap();
|
||||
title = get_foreground_window_title().unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(title, "TITLE_FOOBAR\0".to_owned());
|
||||
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn test_get_active_window_title_doesnt_fail_if_empty_title() {
|
||||
let title;
|
||||
{
|
||||
let window = create_title_window(s!(""), show_window_proc).unwrap();
|
||||
window.set_foreground().unwrap();
|
||||
title = get_foreground_window_title();
|
||||
}
|
||||
|
||||
assert_eq!(title.unwrap(), "".to_owned());
|
||||
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn test_type_input_success() {
|
||||
const TAB: u16 = 0x09;
|
||||
let chars;
|
||||
{
|
||||
let window = create_input_window(w!("foo"), capture_input_proc).unwrap();
|
||||
window.set_foreground().unwrap();
|
||||
|
||||
type_input(
|
||||
&[
|
||||
0x66, 0x6F, 0x6C, 0x6C, 0x6F, 0x77, 0x5F, 0x74, 0x68, 0x65, TAB, 0x77, 0x68, 0x69,
|
||||
0x74, 0x65, 0x5F, 0x72, 0x61, 0x62, 0x62, 0x69, 0x74,
|
||||
],
|
||||
&["Control".to_owned(), "Alt".to_owned(), "B".to_owned()],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Wait for and process input messages
|
||||
window.wait_for_input(250);
|
||||
|
||||
// Verify captured input
|
||||
let capture = window.capture.as_ref().unwrap();
|
||||
chars = capture.get_chars();
|
||||
}
|
||||
|
||||
assert!(!chars.is_empty(), "No input captured");
|
||||
|
||||
let input_str = String::from_iter(chars.iter());
|
||||
let input_str = input_str.replace("\t", "_");
|
||||
|
||||
assert_eq!(input_str, "follow_the_white_rabbit");
|
||||
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
@@ -85,7 +85,8 @@
|
||||
"signIgnore": [
|
||||
"MacOS/desktop_proxy",
|
||||
"MacOS/desktop_proxy.inherit",
|
||||
"Contents/Plugins/autofill-extension.appex"
|
||||
"Contents/Plugins/autofill-extension.appex",
|
||||
"Frameworks/Electron Framework.framework/(Electron Framework|Libraries|Resources|Versions/Current)/.*"
|
||||
],
|
||||
"target": ["dmg", "zip"]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||
</array>
|
||||
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -256,7 +256,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D83832AD2D67B9D0003FB9F8 /* ReleaseDeveloper.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements";
|
||||
CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension_enabled.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
@@ -409,7 +409,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D83832AB2D67B9AE003FB9F8 /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements";
|
||||
CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension_enabled.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "2610"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E1DF713B2B342F6900F29026"
|
||||
BuildableName = "autofill-extension.appex"
|
||||
BlueprintName = "autofill-extension"
|
||||
ReferencedContainer = "container:desktop.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
askForAppToLaunch = "Yes"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "ReleaseAppStore"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
askForAppToLaunch = "Yes"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E1DF713B2B342F6900F29026"
|
||||
BuildableName = "autofill-extension.appex"
|
||||
BlueprintName = "autofill-extension"
|
||||
ReferencedContainer = "container:desktop.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "ReleaseAppStore"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -46,7 +46,7 @@
|
||||
"pack:mac:with-extension": "npm run clean:dist && npm run build:macos-extension:mac && electron-builder --mac --universal -p never",
|
||||
"pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never",
|
||||
"pack:mac:mas": "npm run clean:dist && npm run build:macos-extension:mas && electron-builder --mac mas --universal -p never",
|
||||
"pack:mac:masdev": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never",
|
||||
"pack:mac:masdev": "npm run clean:dist && electron-builder --mac mas-dev --universal -p never -c.mac.identity=null -c.mas.identity=$CSC_NAME -c.mas.provisioningProfile=bitwarden_desktop_developer_id.provisionprofile -c.mas.entitlements=resources/entitlements.mas.autofill-enabled.plist",
|
||||
"pack:local:mac": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never -c.mac.provisioningProfile=\"\" -c.mas.provisioningProfile=\"\"",
|
||||
"pack:win": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never",
|
||||
"pack:win:beta": "npm run clean:dist && electron-builder --config electron-builder.beta.json --win --x64 --arm64 --ia32 -p never",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.application-identifier</key>
|
||||
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>LTZ2PFU5D6</string>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||
</array>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.usb</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
|
||||
<array>
|
||||
<string>/Library/Application Support/Mozilla/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Google/Chrome/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Google/Chrome Beta/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Google/Chrome Dev/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Google/Chrome Canary/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Chromium/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Microsoft Edge/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Microsoft Edge Beta/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Microsoft Edge Dev/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Microsoft Edge Canary/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Vivaldi/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Zen/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/net.imput.helium</string>
|
||||
</array>
|
||||
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -12,9 +12,13 @@ if [ -e "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3" ]; then
|
||||
export LD_PRELOAD="/usr/lib/x86_64-linux-gnu/libdbus-1.so.3"
|
||||
fi
|
||||
|
||||
# A bug in Electron 39 (which now enables Wayland by default) causes a crash on
|
||||
# systems using Wayland with hardware acceleration. Platform decided to
|
||||
# configure Electron to use X11 (with an opt-out) until the upstream bug is
|
||||
# fixed. The follow-up task is https://bitwarden.atlassian.net/browse/PM-31080.
|
||||
PARAMS="--enable-features=UseOzonePlatform,WaylandWindowDecorations --ozone-platform-hint=auto"
|
||||
if [ "$USE_X11" = "true" ]; then
|
||||
PARAMS=""
|
||||
if [ "$USE_X11" != "false" ]; then
|
||||
PARAMS="--ozone-platform=x11"
|
||||
fi
|
||||
|
||||
$APP_PATH/bitwarden-app $PARAMS "$@"
|
||||
|
||||
@@ -16,7 +16,9 @@ async function run(context) {
|
||||
const appPath = `${context.appOutDir}/${appName}.app`;
|
||||
const macBuild = context.electronPlatformName === "darwin";
|
||||
const copySafariExtension = ["darwin", "mas"].includes(context.electronPlatformName);
|
||||
const copyAutofillExtension = ["darwin"].includes(context.electronPlatformName); // Disabled for mas builds
|
||||
const isMasDevBuild =
|
||||
context.electronPlatformName === "mas" && context.targets.at(0)?.name === "mas-dev";
|
||||
const copyAutofillExtension = ["darwin"].includes(context.electronPlatformName) || isMasDevBuild;
|
||||
|
||||
let shouldResign = false;
|
||||
|
||||
@@ -31,7 +33,6 @@ async function run(context) {
|
||||
fse.mkdirSync(path.join(appPath, "Contents/PlugIns"));
|
||||
}
|
||||
fse.copySync(extensionPath, path.join(appPath, "Contents/PlugIns/autofill-extension.appex"));
|
||||
shouldResign = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<bit-layout class="!tw-h-full">
|
||||
<bit-layout class="!tw-h-full" rounded>
|
||||
<app-side-nav slot="side-nav">
|
||||
<bit-nav-logo [openIcon]="logo" route="." [label]="'passwordManager' | i18n" />
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ import {
|
||||
PlatformUtilsService,
|
||||
PlatformUtilsService as PlatformUtilsServiceAbstraction,
|
||||
} from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
@@ -432,6 +433,7 @@ const safeProviders: SafeProvider[] = [
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
AccountCryptographicStateService,
|
||||
RegisterSdkService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { DefaultSetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/default-set-initial-password.service.implementation";
|
||||
import {
|
||||
InitializeJitPasswordCredentials,
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordUserType,
|
||||
@@ -19,12 +21,14 @@ import { AccountCryptographicStateService } from "@bitwarden/common/key-manageme
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -45,6 +49,7 @@ describe("DesktopSetInitialPasswordService", () => {
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let accountCryptographicStateService: MockProxy<AccountCryptographicStateService>;
|
||||
let registerSdkService: MockProxy<RegisterSdkService>;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
@@ -59,6 +64,7 @@ describe("DesktopSetInitialPasswordService", () => {
|
||||
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
messagingService = mock<MessagingService>();
|
||||
accountCryptographicStateService = mock<AccountCryptographicStateService>();
|
||||
registerSdkService = mock<RegisterSdkService>();
|
||||
|
||||
sut = new DesktopSetInitialPasswordService(
|
||||
apiService,
|
||||
@@ -73,6 +79,7 @@ describe("DesktopSetInitialPasswordService", () => {
|
||||
userDecryptionOptionsService,
|
||||
messagingService,
|
||||
accountCryptographicStateService,
|
||||
registerSdkService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -179,4 +186,36 @@ describe("DesktopSetInitialPasswordService", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("initializePasswordJitPasswordUserV2Encryption(...)", () => {
|
||||
it("should send a 'redrawMenu' message", async () => {
|
||||
// Arrange
|
||||
const credentials: InitializeJitPasswordCredentials = {
|
||||
newPasswordHint: "newPasswordHint",
|
||||
orgSsoIdentifier: "orgSsoIdentifier",
|
||||
orgId: "orgId" as OrganizationId,
|
||||
resetPasswordAutoEnroll: false,
|
||||
newPassword: "newPassword123!",
|
||||
salt: "user@example.com" as MasterPasswordSalt,
|
||||
};
|
||||
const userId = "userId" as UserId;
|
||||
|
||||
const superSpy = jest
|
||||
.spyOn(
|
||||
DefaultSetInitialPasswordService.prototype,
|
||||
"initializePasswordJitPasswordUserV2Encryption",
|
||||
)
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
await sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId);
|
||||
|
||||
// Assert
|
||||
expect(superSpy).toHaveBeenCalledWith(credentials, userId);
|
||||
expect(messagingService.send).toHaveBeenCalledTimes(1);
|
||||
expect(messagingService.send).toHaveBeenCalledWith("redrawMenu");
|
||||
|
||||
superSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { DefaultSetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/default-set-initial-password.service.implementation";
|
||||
import {
|
||||
InitializeJitPasswordCredentials,
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordUserType,
|
||||
@@ -14,6 +15,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -34,6 +36,7 @@ export class DesktopSetInitialPasswordService
|
||||
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
private messagingService: MessagingService,
|
||||
protected accountCryptographicStateService: AccountCryptographicStateService,
|
||||
protected registerSdkService: RegisterSdkService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
@@ -47,6 +50,7 @@ export class DesktopSetInitialPasswordService
|
||||
organizationUserApiService,
|
||||
userDecryptionOptionsService,
|
||||
accountCryptographicStateService,
|
||||
registerSdkService,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,4 +63,13 @@ export class DesktopSetInitialPasswordService
|
||||
|
||||
this.messagingService.send("redrawMenu");
|
||||
}
|
||||
|
||||
override async initializePasswordJitPasswordUserV2Encryption(
|
||||
credentials: InitializeJitPasswordCredentials,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await super.initializePasswordJitPasswordUserV2Encryption(credentials, userId);
|
||||
|
||||
this.messagingService.send("redrawMenu");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,93 @@
|
||||
<!-- Header with Send title and New button -->
|
||||
<app-header>
|
||||
@if (!disableSend()) {
|
||||
<tools-new-send-dropdown-v2 buttonType="primary" (addSend)="addSend($event)" />
|
||||
}
|
||||
</app-header>
|
||||
<!-- Send List Component -->
|
||||
<tools-send-list
|
||||
[sends]="filteredSends()"
|
||||
[loading]="loading()"
|
||||
[disableSend]="disableSend()"
|
||||
[listState]="listState()"
|
||||
[searchText]="currentSearchText()"
|
||||
(editSend)="onEditSend($event)"
|
||||
(copySend)="onCopySend($event)"
|
||||
(deleteSend)="onDeleteSend($event)"
|
||||
(removePassword)="onRemovePassword($event)"
|
||||
>
|
||||
<tools-new-send-dropdown-v2
|
||||
slot="empty-button"
|
||||
[hideIcon]="true"
|
||||
buttonType="primary"
|
||||
(addSend)="addSend($event)"
|
||||
/>
|
||||
</tools-send-list>
|
||||
@if (useDrawerEditMode()) {
|
||||
<div class="tw-m-4 tw-p-4">
|
||||
<!-- New dialog-based layout (feature flag enabled) -->
|
||||
<!-- Header with Send title and New button -->
|
||||
<app-header>
|
||||
@if (!disableSend()) {
|
||||
<tools-new-send-dropdown-v2 buttonType="primary" (addSend)="addSend($event)" />
|
||||
}
|
||||
</app-header>
|
||||
<!-- Send List Component -->
|
||||
<tools-send-list
|
||||
[sends]="filteredSends()"
|
||||
[loading]="loading()"
|
||||
[disableSend]="disableSend()"
|
||||
[listState]="listState()"
|
||||
[searchText]="currentSearchText()"
|
||||
(editSend)="onEditSend($event)"
|
||||
(copySend)="onCopySend($event)"
|
||||
(deleteSend)="onDeleteSend($event)"
|
||||
(removePassword)="onRemovePassword($event)"
|
||||
>
|
||||
<tools-new-send-dropdown-v2
|
||||
slot="empty-button"
|
||||
[hideIcon]="true"
|
||||
buttonType="primary"
|
||||
(addSend)="addSend($event)"
|
||||
/>
|
||||
</tools-send-list>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Old split-panel layout (feature flag disabled) -->
|
||||
<div id="sends" class="vault">
|
||||
<div class="send-items-panel tw-w-2/5">
|
||||
<!-- Header with Send title and New button -->
|
||||
<app-header class="tw-block tw-pt-6 tw-px-6">
|
||||
@if (!disableSend()) {
|
||||
<button type="button" bitButton buttonType="primary" (click)="addSendWithoutType()">
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
{{ "new" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</app-header>
|
||||
<div class="tw-mb-4 tw-px-6">
|
||||
<!-- Send List Component -->
|
||||
<tools-send-list
|
||||
[sends]="filteredSends()"
|
||||
[loading]="loading()"
|
||||
[disableSend]="disableSend()"
|
||||
[listState]="listState()"
|
||||
[searchText]="currentSearchText()"
|
||||
(editSend)="onEditSend($event)"
|
||||
(copySend)="onCopySend($event)"
|
||||
(deleteSend)="onDeleteSend($event)"
|
||||
(removePassword)="onRemovePassword($event)"
|
||||
>
|
||||
<button
|
||||
slot="empty-button"
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="addSendWithoutType()"
|
||||
>
|
||||
{{ "newSend" | i18n }}
|
||||
</button>
|
||||
</tools-send-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit/Add panel (right side) -->
|
||||
@if (action() == "add" || action() == "edit") {
|
||||
<app-send-add-edit
|
||||
id="addEdit"
|
||||
class="details"
|
||||
[sendId]="sendId()"
|
||||
[type]="selectedSendType()"
|
||||
(onSavedSend)="savedSend($event)"
|
||||
(onCancelled)="closeEditPanel()"
|
||||
(onDeletedSend)="closeEditPanel()"
|
||||
></app-send-add-edit>
|
||||
}
|
||||
|
||||
<!-- Bitwarden logo (shown when no send is selected) -->
|
||||
@if (!action()) {
|
||||
<div class="logo tw-w-1/2">
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<img class="logo-image" alt="Bitwarden" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ describe("SendV2Component", () => {
|
||||
let sendApiService: MockProxy<SendApiService>;
|
||||
let toastService: MockProxy<ToastService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
sendService = mock<SendService>();
|
||||
@@ -62,6 +63,10 @@ describe("SendV2Component", () => {
|
||||
sendApiService = mock<SendApiService>();
|
||||
toastService = mock<ToastService>();
|
||||
i18nService = mock<I18nService>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
// Setup configService mock - feature flag returns true to test the new drawer mode
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
// Setup environmentService mock
|
||||
environmentService.getEnvironment.mockResolvedValue({
|
||||
@@ -117,7 +122,7 @@ describe("SendV2Component", () => {
|
||||
useValue: mock<BillingAccountProfileStateService>(),
|
||||
},
|
||||
{ provide: MessagingService, useValue: mock<MessagingService>() },
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
signal,
|
||||
viewChild,
|
||||
} from "@angular/core";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { combineLatest, map, switchMap, lastValueFrom } from "rxjs";
|
||||
@@ -15,6 +17,8 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -36,12 +40,27 @@ import {
|
||||
|
||||
import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service";
|
||||
import { DesktopHeaderComponent } from "../../layout/header";
|
||||
import { AddEditComponent } from "../send/add-edit.component";
|
||||
|
||||
const Action = Object.freeze({
|
||||
/** No action is currently active. */
|
||||
None: "",
|
||||
/** The user is adding a new Send. */
|
||||
Add: "add",
|
||||
/** The user is editing an existing Send. */
|
||||
Edit: "edit",
|
||||
} as const);
|
||||
|
||||
type Action = (typeof Action)[keyof typeof Action];
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-send-v2",
|
||||
imports: [
|
||||
JslibModule,
|
||||
ButtonModule,
|
||||
AddEditComponent,
|
||||
SendListComponent,
|
||||
NewSendDropdownV2Component,
|
||||
DesktopHeaderComponent,
|
||||
@@ -54,13 +73,19 @@ import { DesktopHeaderComponent } from "../../layout/header";
|
||||
},
|
||||
],
|
||||
templateUrl: "./send-v2.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SendV2Component {
|
||||
protected readonly addEditComponent = viewChild(AddEditComponent);
|
||||
|
||||
protected readonly sendId = signal<string | null>(null);
|
||||
protected readonly action = signal<Action>(Action.None);
|
||||
private readonly selectedSendTypeOverride = signal<SendType | undefined>(undefined);
|
||||
|
||||
private sendFormConfigService = inject(DefaultSendFormConfigService);
|
||||
private sendItemsService = inject(SendItemsService);
|
||||
private policyService = inject(PolicyService);
|
||||
private accountService = inject(AccountService);
|
||||
private configService = inject(ConfigService);
|
||||
private i18nService = inject(I18nService);
|
||||
private platformUtilsService = inject(PlatformUtilsService);
|
||||
private environmentService = inject(EnvironmentService);
|
||||
@@ -70,6 +95,11 @@ export class SendV2Component {
|
||||
private logService = inject(LogService);
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
|
||||
protected readonly useDrawerEditMode = toSignal(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.DesktopUiMigrationMilestone2),
|
||||
{ initialValue: false },
|
||||
);
|
||||
|
||||
protected readonly filteredSends = toSignal(this.sendItemsService.filteredAndSortedSends$, {
|
||||
initialValue: [],
|
||||
});
|
||||
@@ -119,28 +149,79 @@ export class SendV2Component {
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly selectedSendType = computed(() => {
|
||||
const action = this.action();
|
||||
const typeOverride = this.selectedSendTypeOverride();
|
||||
|
||||
if (action === Action.Add && typeOverride !== undefined) {
|
||||
return typeOverride;
|
||||
}
|
||||
|
||||
const sendId = this.sendId();
|
||||
return this.filteredSends().find((s) => s.id === sendId)?.type;
|
||||
});
|
||||
|
||||
protected async addSend(type: SendType): Promise<void> {
|
||||
const formConfig = await this.sendFormConfigService.buildConfig("add", undefined, type);
|
||||
if (this.useDrawerEditMode()) {
|
||||
const formConfig = await this.sendFormConfigService.buildConfig("add", undefined, type);
|
||||
|
||||
const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, {
|
||||
formConfig,
|
||||
});
|
||||
const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, {
|
||||
formConfig,
|
||||
});
|
||||
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
} else {
|
||||
this.action.set(Action.Add);
|
||||
this.sendId.set(null);
|
||||
this.selectedSendTypeOverride.set(type);
|
||||
|
||||
const component = this.addEditComponent();
|
||||
if (component) {
|
||||
await component.resetAndLoad();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async selectSend(sendId: SendId): Promise<void> {
|
||||
const formConfig = await this.sendFormConfigService.buildConfig("edit", sendId);
|
||||
/** Used by old UI to add a send without specifying type (defaults to Text) */
|
||||
protected async addSendWithoutType(): Promise<void> {
|
||||
await this.addSend(SendType.Text);
|
||||
}
|
||||
|
||||
const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, {
|
||||
formConfig,
|
||||
});
|
||||
protected closeEditPanel(): void {
|
||||
this.action.set(Action.None);
|
||||
this.sendId.set(null);
|
||||
this.selectedSendTypeOverride.set(undefined);
|
||||
}
|
||||
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
protected async savedSend(send: SendView): Promise<void> {
|
||||
await this.selectSend(send.id);
|
||||
}
|
||||
|
||||
protected async selectSend(sendId: string): Promise<void> {
|
||||
if (this.useDrawerEditMode()) {
|
||||
const formConfig = await this.sendFormConfigService.buildConfig("edit", sendId as SendId);
|
||||
|
||||
const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, {
|
||||
formConfig,
|
||||
});
|
||||
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
} else {
|
||||
if (sendId === this.sendId() && this.action() === Action.Edit) {
|
||||
return;
|
||||
}
|
||||
this.action.set(Action.Edit);
|
||||
this.sendId.set(sendId);
|
||||
const component = this.addEditComponent();
|
||||
if (component) {
|
||||
component.sendId = sendId;
|
||||
await component.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async onEditSend(send: SendView): Promise<void> {
|
||||
await this.selectSend(send.id as SendId);
|
||||
await this.selectSend(send.id);
|
||||
}
|
||||
|
||||
protected async onCopySend(send: SendView): Promise<void> {
|
||||
@@ -176,6 +257,11 @@ export class SendV2Component {
|
||||
title: null,
|
||||
message: this.i18nService.t("removedPassword"),
|
||||
});
|
||||
|
||||
if (!this.useDrawerEditMode() && this.sendId() === send.id) {
|
||||
this.sendId.set(null);
|
||||
await this.selectSend(send.id);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
@@ -199,5 +285,9 @@ export class SendV2Component {
|
||||
title: null,
|
||||
message: this.i18nService.t("deletedSend"),
|
||||
});
|
||||
|
||||
if (!this.useDrawerEditMode()) {
|
||||
this.closeEditPanel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
mergeMap,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -52,6 +53,8 @@ import type { NativeWindowObject } from "./desktop-fido2-user-interface.service"
|
||||
export class DesktopAutofillService implements OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
private registrationRequest: autofill.PasskeyRegistrationRequest;
|
||||
private featureFlag?: FeatureFlag;
|
||||
private isEnabled: boolean = false;
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
@@ -60,19 +63,26 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction<NativeWindowObject>,
|
||||
private accountService: AccountService,
|
||||
private authService: AuthService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {}
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
) {
|
||||
const deviceType = platformUtilsService.getDevice();
|
||||
if (deviceType === DeviceType.MacOsDesktop) {
|
||||
this.featureFlag = FeatureFlag.MacOsNativeCredentialSync;
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Currently only supported for MacOS
|
||||
if (this.platformUtilsService.getDevice() !== DeviceType.MacOsDesktop) {
|
||||
this.isEnabled =
|
||||
this.featureFlag && (await this.configService.getFeatureFlag(this.featureFlag));
|
||||
if (!this.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.MacOsNativeCredentialSync)
|
||||
.getFeatureFlag$(this.featureFlag)
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
tap((enabled) => (this.isEnabled = enabled)),
|
||||
filter((enabled) => enabled === true), // Only proceed if feature is enabled
|
||||
switchMap(() => {
|
||||
return combineLatest([
|
||||
@@ -199,11 +209,11 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
|
||||
listenIpc() {
|
||||
ipc.autofill.listenPasskeyRegistration(async (clientId, sequenceNumber, request, callback) => {
|
||||
if (!(await this.configService.getFeatureFlag(FeatureFlag.MacOsNativeCredentialSync))) {
|
||||
if (!this.isEnabled) {
|
||||
this.logService.debug(
|
||||
"listenPasskeyRegistration: MacOsNativeCredentialSync feature flag is disabled",
|
||||
`listenPasskeyRegistration: Native credential sync feature flag (${this.featureFlag}) is disabled`,
|
||||
);
|
||||
callback(new Error("MacOsNativeCredentialSync feature flag is disabled"), null);
|
||||
callback(new Error("Native credential sync feature flag is disabled"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -230,11 +240,11 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
|
||||
ipc.autofill.listenPasskeyAssertionWithoutUserInterface(
|
||||
async (clientId, sequenceNumber, request, callback) => {
|
||||
if (!(await this.configService.getFeatureFlag(FeatureFlag.MacOsNativeCredentialSync))) {
|
||||
if (!this.isEnabled) {
|
||||
this.logService.debug(
|
||||
"listenPasskeyAssertionWithoutUserInterface: MacOsNativeCredentialSync feature flag is disabled",
|
||||
`listenPasskeyAssertionWithoutUserInterface: Native credential sync feature flag (${this.featureFlag}) is disabled`,
|
||||
);
|
||||
callback(new Error("MacOsNativeCredentialSync feature flag is disabled"), null);
|
||||
callback(new Error("Native credential sync feature flag is disabled"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -297,11 +307,11 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
);
|
||||
|
||||
ipc.autofill.listenPasskeyAssertion(async (clientId, sequenceNumber, request, callback) => {
|
||||
if (!(await this.configService.getFeatureFlag(FeatureFlag.MacOsNativeCredentialSync))) {
|
||||
if (!this.isEnabled) {
|
||||
this.logService.debug(
|
||||
"listenPasskeyAssertion: MacOsNativeCredentialSync feature flag is disabled",
|
||||
`listenPasskeyAssertion: Native credential sync feature flag (${this.featureFlag}) is disabled`,
|
||||
);
|
||||
callback(new Error("MacOsNativeCredentialSync feature flag is disabled"), null);
|
||||
callback(new Error("Native credential sync feature flag is disabled"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -324,9 +334,9 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
|
||||
// Listen for native status messages
|
||||
ipc.autofill.listenNativeStatus(async (clientId, sequenceNumber, status) => {
|
||||
if (!(await this.configService.getFeatureFlag(FeatureFlag.MacOsNativeCredentialSync))) {
|
||||
if (!this.isEnabled) {
|
||||
this.logService.debug(
|
||||
"listenNativeStatus: MacOsNativeCredentialSync feature flag is disabled",
|
||||
`listenNativeStatus: Native credential sync feature flag (${this.featureFlag}) is disabled`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { ApproveSshRequestComponent } from "../../platform/components/approve-ssh-request";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
import { ApproveSshRequestComponent } from "../components/approve-ssh-request";
|
||||
import { SshAgentPromptType } from "../models/ssh-agent-setting";
|
||||
|
||||
@Injectable({
|
||||
|
||||
29
apps/desktop/src/scss/migration.scss
Normal file
29
apps/desktop/src/scss/migration.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Desktop UI Migration
|
||||
*
|
||||
* These are temporary styles during the desktop ui migration.
|
||||
**/
|
||||
|
||||
/**
|
||||
* This removes any padding applied by the bit-layout to content.
|
||||
* This should be revisited once the table is migrated, and again once drawers are migrated.
|
||||
**/
|
||||
bit-layout {
|
||||
#main-content {
|
||||
padding: 0 0 0 0;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Send list panel styling for send-v2 component
|
||||
* Temporary during migration - width handled by tw-w-2/5
|
||||
**/
|
||||
.vault > .send-items-panel {
|
||||
order: 2;
|
||||
min-width: 200px;
|
||||
border-right: 1px solid;
|
||||
|
||||
@include themify($themes) {
|
||||
background-color: themed("backgroundColor");
|
||||
border-right-color: themed("borderColor");
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,6 @@
|
||||
@import "left-nav.scss";
|
||||
@import "loading.scss";
|
||||
@import "plugins.scss";
|
||||
@import "migration.scss";
|
||||
@import "../../../../libs/angular/src/scss/icons.scss";
|
||||
@import "../../../../libs/components/src/multi-select/scss/bw.theme";
|
||||
|
||||
@@ -36,15 +36,11 @@
|
||||
>
|
||||
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
*ngIf="cipher.id && !cipher?.organizationId && !cipher.isDeleted && action === 'view'"
|
||||
(click)="clone()"
|
||||
appA11yTitle="{{ 'clone' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
@if (showCloneOption) {
|
||||
<button type="button" class="primary" (click)="clone()" appA11yTitle="{{ 'clone' | i18n }}">
|
||||
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
}
|
||||
</ng-container>
|
||||
<div class="right" *ngIf="hasFooterAction">
|
||||
<button
|
||||
|
||||
@@ -75,6 +75,7 @@ export class ItemFooterComponent implements OnInit, OnChanges {
|
||||
|
||||
protected showArchiveButton = false;
|
||||
protected showUnarchiveButton = false;
|
||||
protected userCanArchive = false;
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
@@ -134,6 +135,16 @@ export class ItemFooterComponent implements OnInit, OnChanges {
|
||||
);
|
||||
}
|
||||
|
||||
protected get showCloneOption() {
|
||||
return (
|
||||
this.cipher.id &&
|
||||
!this.cipher?.organizationId &&
|
||||
!this.cipher.isDeleted &&
|
||||
this.action === "view" &&
|
||||
(!this.cipher.isArchived || this.userCanArchive)
|
||||
);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCancel.emit(this.cipher);
|
||||
}
|
||||
@@ -241,6 +252,8 @@ export class ItemFooterComponent implements OnInit, OnChanges {
|
||||
),
|
||||
);
|
||||
|
||||
this.userCanArchive = userCanArchive;
|
||||
|
||||
this.showArchiveButton =
|
||||
cipherCanBeArchived && userCanArchive && this.action === "view" && !this.cipher.isArchived;
|
||||
|
||||
|
||||
@@ -10,69 +10,79 @@
|
||||
[organizationId]="organizationId"
|
||||
>
|
||||
</app-vault-items-v2>
|
||||
<div class="details" *ngIf="!!action">
|
||||
<app-vault-item-footer
|
||||
id="footer"
|
||||
#footer
|
||||
[cipher]="cipher()"
|
||||
[action]="action"
|
||||
(onEdit)="editCipher($event)"
|
||||
(onRestore)="restoreCipher()"
|
||||
(onClone)="cloneCipher($event)"
|
||||
(onDelete)="deleteCipher()"
|
||||
(onCancel)="cancelCipher($event)"
|
||||
(onArchiveToggle)="refreshCurrentCipher()"
|
||||
[masterPasswordAlreadyPrompted]="cipherRepromptId === cipherId"
|
||||
[submitButtonText]="submitButtonText()"
|
||||
></app-vault-item-footer>
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<div class="box">
|
||||
<app-cipher-view
|
||||
*ngIf="action === 'view'"
|
||||
[cipher]="cipher()"
|
||||
[collections]="collections"
|
||||
>
|
||||
</app-cipher-view>
|
||||
<vault-cipher-form
|
||||
#vaultForm
|
||||
*ngIf="action === 'add' || action === 'edit' || action === 'clone'"
|
||||
formId="cipherForm"
|
||||
[config]="config"
|
||||
(cipherSaved)="savedCipher($event)"
|
||||
[submitBtn]="footer?.submitBtn"
|
||||
(formStatusChange$)="formStatusChanged($event)"
|
||||
>
|
||||
<bit-item slot="attachment-button">
|
||||
<button
|
||||
bit-item-content
|
||||
type="button"
|
||||
(click)="openAttachmentsDialog()"
|
||||
[disabled]="formDisabled"
|
||||
@if (!!action) {
|
||||
<div class="details">
|
||||
<app-vault-item-footer
|
||||
id="footer"
|
||||
#footer
|
||||
[cipher]="cipher()"
|
||||
[action]="action"
|
||||
(onEdit)="editCipher($event)"
|
||||
(onRestore)="restoreCipher()"
|
||||
(onClone)="cloneCipher($event)"
|
||||
(onDelete)="deleteCipher()"
|
||||
(onCancel)="cancelCipher($event)"
|
||||
(onArchiveToggle)="refreshCurrentCipher()"
|
||||
[masterPasswordAlreadyPrompted]="cipherRepromptId === cipherId"
|
||||
[submitButtonText]="submitButtonText()"
|
||||
></app-vault-item-footer>
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<div class="box">
|
||||
@if (action === "view") {
|
||||
<app-cipher-view [cipher]="cipher()" [collections]="collections"> </app-cipher-view>
|
||||
}
|
||||
@if (action === "add" || action === "edit" || action === "clone") {
|
||||
<vault-cipher-form
|
||||
#vaultForm
|
||||
formId="cipherForm"
|
||||
[config]="config"
|
||||
(cipherSaved)="savedCipher($event)"
|
||||
[submitBtn]="footer?.submitBtn"
|
||||
(formStatusChange$)="formStatusChanged($event)"
|
||||
>
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
{{ "attachments" | i18n }}
|
||||
<app-premium-badge></app-premium-badge>
|
||||
</div>
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-item>
|
||||
</vault-cipher-form>
|
||||
<bit-item slot="attachment-button">
|
||||
<button
|
||||
bit-item-content
|
||||
type="button"
|
||||
(click)="openAttachmentsDialog()"
|
||||
[disabled]="formDisabled"
|
||||
>
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
{{ "attachments" | i18n }}
|
||||
<app-premium-badge></app-premium-badge>
|
||||
</div>
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-item>
|
||||
</vault-cipher-form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="logo"
|
||||
class="logo"
|
||||
*ngIf="action !== 'add' && action !== 'edit' && action !== 'view' && action !== 'clone'"
|
||||
>
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<img class="logo-image" alt="Bitwarden" aria-hidden="true" />
|
||||
}
|
||||
@if (action !== "add" && action !== "edit" && action !== "view" && action !== "clone") {
|
||||
<div id="logo" class="logo">
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
@if (activeFilter.status === "archive" && !(hasArchivedCiphers$ | async)) {
|
||||
<bit-no-items [icon]="itemTypesIcon">
|
||||
<div slot="title">
|
||||
{{ "noItemsInArchive" | i18n }}
|
||||
</div>
|
||||
<p slot="description" bitTypography="body2" class="tw-max-w-md tw-text-center">
|
||||
{{ "noItemsInArchiveDesc" | i18n }}
|
||||
</p>
|
||||
</bit-no-items>
|
||||
} @else {
|
||||
<img class="logo-image" alt="Bitwarden" aria-hidden="true" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="left-nav">
|
||||
<app-vault-filter
|
||||
class="vault-filters"
|
||||
|
||||
@@ -27,6 +27,7 @@ import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
import { ItemTypes } from "@bitwarden/assets/svg";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
@@ -69,6 +70,7 @@ import {
|
||||
ToastService,
|
||||
CopyClickListener,
|
||||
COPY_CLICK_LISTENER,
|
||||
NoItemsModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import {
|
||||
@@ -124,6 +126,7 @@ const BroadcasterSubscriptionId = "VaultComponent";
|
||||
NavComponent,
|
||||
VaultFilterModule,
|
||||
VaultItemsV2Component,
|
||||
NoItemsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -203,6 +206,7 @@ export class VaultV2Component<C extends CipherViewLike>
|
||||
collections: CollectionView[] | null = null;
|
||||
config: CipherFormConfig | null = null;
|
||||
readonly userHasPremium = signal<boolean>(false);
|
||||
protected itemTypesIcon = ItemTypes;
|
||||
|
||||
/** Tracks the disabled status of the edit cipher form */
|
||||
protected formDisabled: boolean = false;
|
||||
@@ -221,6 +225,12 @@ export class VaultV2Component<C extends CipherViewLike>
|
||||
: this.i18nService.t("save");
|
||||
});
|
||||
|
||||
protected hasArchivedCiphers$ = this.userId$.pipe(
|
||||
switchMap((userId) =>
|
||||
this.cipherArchiveService.archivedCiphers$(userId).pipe(map((ciphers) => ciphers.length > 0)),
|
||||
),
|
||||
);
|
||||
|
||||
private componentIsDestroyed$ = new Subject<boolean>();
|
||||
private allOrganizations: Organization[] = [];
|
||||
private allCollections: CollectionView[] = [];
|
||||
|
||||
@@ -2,6 +2,6 @@ export { PoliciesComponent } from "./policies.component";
|
||||
export { ossPolicyEditRegister } from "./policy-edit-register";
|
||||
export { BasePolicyEditDefinition, BasePolicyEditComponent } from "./base-policy-edit.component";
|
||||
export { POLICY_EDIT_REGISTER } from "./policy-register-token";
|
||||
export { AutoConfirmPolicyDialogComponent } from "./auto-confirm-edit-policy-dialog.component";
|
||||
export { AutoConfirmPolicy } from "./policy-edit-definitions";
|
||||
export { PolicyEditDialogResult } from "./policy-edit-dialog.component";
|
||||
export * from "./policy-edit-dialogs";
|
||||
|
||||
@@ -15,8 +15,8 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { AutoConfirmPolicyDialogComponent } from "../auto-confirm-edit-policy-dialog.component";
|
||||
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
|
||||
import { AutoConfirmPolicyDialogComponent } from "../policy-edit-dialogs/auto-confirm-edit-policy-dialog.component";
|
||||
|
||||
export class AutoConfirmPolicy extends BasePolicyEditDefinition {
|
||||
name = "autoConfirm";
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
export { DisableSendPolicy } from "./disable-send.component";
|
||||
export { DesktopAutotypeDefaultSettingPolicy } from "./autotype-policy.component";
|
||||
export { MasterPasswordPolicy } from "./master-password.component";
|
||||
export { OrganizationDataOwnershipPolicy } from "./organization-data-ownership.component";
|
||||
export {
|
||||
OrganizationDataOwnershipPolicy,
|
||||
OrganizationDataOwnershipPolicyComponent,
|
||||
} from "./organization-data-ownership.component";
|
||||
export { PasswordGeneratorPolicy } from "./password-generator.component";
|
||||
export { RemoveUnlockWithPinPolicy } from "./remove-unlock-with-pin.component";
|
||||
export { RequireSsoPolicy } from "./require-sso.component";
|
||||
|
||||
@@ -1,8 +1,57 @@
|
||||
<bit-callout type="warning">
|
||||
{{ "personalOwnershipExemption" | i18n }}
|
||||
</bit-callout>
|
||||
<p>
|
||||
{{ "organizationDataOwnershipDescContent" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/resources/credential-lifecycle-management/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ "organizationDataOwnershipContentAnchor" | i18n }}.
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<ng-template #dialog>
|
||||
<bit-simple-dialog background="alt">
|
||||
<span bitDialogTitle>{{ "organizationDataOwnershipWarningTitle" | i18n }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<div class="tw-text-left tw-overflow-hidden">
|
||||
{{ "organizationDataOwnershipWarningContentTop" | i18n }}
|
||||
<div class="tw-flex tw-flex-col tw-p-2">
|
||||
<ul class="tw-list-disc tw-pl-5 tw-space-y-2 tw-break-words tw-mb-0">
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning1" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning2" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning3" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ "organizationDataOwnershipWarningContentBottom" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/resources/credential-lifecycle-management/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ "organizationDataOwnershipContentAnchor" | i18n }}.
|
||||
</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<span class="tw-flex tw-gap-2">
|
||||
<button bitButton buttonType="primary" [bitDialogClose]="true" type="submit">
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" [bitDialogClose]="false" type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
import { of, Observable } from "rxjs";
|
||||
import { ChangeDetectionStrategy, Component, OnInit, TemplateRef, ViewChild } from "@angular/core";
|
||||
import { lastValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { CenterPositionStrategy, DialogService } from "@bitwarden/components";
|
||||
import { EncString } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
|
||||
|
||||
export interface VNextPolicyRequest {
|
||||
policy: PolicyRequest;
|
||||
metadata: {
|
||||
defaultUserCollectionName: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class OrganizationDataOwnershipPolicy extends BasePolicyEditDefinition {
|
||||
name = "organizationDataOwnership";
|
||||
description = "personalOwnershipPolicyDesc";
|
||||
description = "organizationDataOwnershipDesc";
|
||||
type = PolicyType.OrganizationDataOwnership;
|
||||
component = OrganizationDataOwnershipPolicyComponent;
|
||||
showDescription = false;
|
||||
|
||||
display$(organization: Organization, configService: ConfigService): Observable<boolean> {
|
||||
// TODO Remove this entire component upon verifying that it can be deleted due to its sole reliance of the CreateDefaultLocation feature flag
|
||||
return of(false);
|
||||
override display$(organization: Organization, configService: ConfigService): Observable<boolean> {
|
||||
return configService
|
||||
.getFeatureFlag$(FeatureFlag.MigrateMyVaultToMyItems)
|
||||
.pipe(map((enabled) => !enabled));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +42,61 @@ export class OrganizationDataOwnershipPolicy extends BasePolicyEditDefinition {
|
||||
imports: [SharedModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OrganizationDataOwnershipPolicyComponent extends BasePolicyEditComponent {}
|
||||
export class OrganizationDataOwnershipPolicyComponent
|
||||
extends BasePolicyEditComponent
|
||||
implements OnInit
|
||||
{
|
||||
constructor(
|
||||
private dialogService: DialogService,
|
||||
private i18nService: I18nService,
|
||||
private encryptService: EncryptService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild("dialog", { static: true }) warningContent!: TemplateRef<unknown>;
|
||||
|
||||
override async confirm(): Promise<boolean> {
|
||||
if (this.policyResponse?.enabled && !this.enabled.value) {
|
||||
const dialogRef = this.dialogService.open(this.warningContent, {
|
||||
positionStrategy: new CenterPositionStrategy(),
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
return Boolean(result);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async buildVNextRequest(orgKey: OrgKey): Promise<VNextPolicyRequest> {
|
||||
if (!this.policy) {
|
||||
throw new Error("Policy was not found");
|
||||
}
|
||||
|
||||
const defaultUserCollectionName = await this.getEncryptedDefaultUserCollectionName(orgKey);
|
||||
|
||||
const request: VNextPolicyRequest = {
|
||||
policy: {
|
||||
enabled: this.enabled.value ?? false,
|
||||
data: this.buildRequestData(),
|
||||
},
|
||||
metadata: {
|
||||
defaultUserCollectionName,
|
||||
},
|
||||
};
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private async getEncryptedDefaultUserCollectionName(orgKey: OrgKey): Promise<EncString> {
|
||||
const defaultCollectionName = this.i18nService.t("myItems");
|
||||
const encrypted = await this.encryptService.encryptString(defaultCollectionName, orgKey);
|
||||
|
||||
if (!encrypted.encryptedString) {
|
||||
throw new Error("Encryption error");
|
||||
}
|
||||
|
||||
return encrypted.encryptedString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,52 @@
|
||||
<p>
|
||||
{{ "organizationDataOwnershipDescContent" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/resources/credential-lifecycle-management/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ "organizationDataOwnershipContentAnchor" | i18n }}.
|
||||
</a>
|
||||
</p>
|
||||
<ng-container [ngTemplateOutlet]="steps[step()]()"></ng-container>
|
||||
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<ng-template #step0>
|
||||
<p>
|
||||
{{ "centralizeDataOwnershipDesc" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/resources/credential-lifecycle-management/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ "centralizeDataOwnershipContentAnchor" | i18n }}
|
||||
<i class="bwi bwi-external-link"></i>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<ng-template #dialog>
|
||||
<bit-simple-dialog background="alt">
|
||||
<span bitDialogTitle>{{ "organizationDataOwnershipWarningTitle" | i18n }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<div class="tw-text-left tw-overflow-hidden">
|
||||
{{ "organizationDataOwnershipWarningContentTop" | i18n }}
|
||||
<div class="tw-flex tw-flex-col tw-p-2">
|
||||
<ul class="tw-list-disc tw-pl-5 tw-space-y-2 tw-break-words tw-mb-0">
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning1" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning2" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning3" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ "organizationDataOwnershipWarningContentBottom" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/resources/credential-lifecycle-management/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ "organizationDataOwnershipContentAnchor" | i18n }}.
|
||||
</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<span class="tw-flex tw-gap-2">
|
||||
<button bitButton buttonType="primary" [bitDialogClose]="true" type="submit">
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" [bitDialogClose]="false" type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
<div class="tw-text-left tw-overflow-hidden tw-mb-2">
|
||||
<strong>{{ "benefits" | i18n }}:</strong>
|
||||
<ul class="tw-pl-7 tw-space-y-2 tw-pt-2">
|
||||
<li>
|
||||
{{ "centralizeDataOwnershipBenefit1" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
{{ "centralizeDataOwnershipBenefit2" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
{{ "centralizeDataOwnershipBenefit3" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<bit-form-control>
|
||||
<input class="tw-mt-4" type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #step1>
|
||||
<div class="tw-flex tw-flex-col tw-gap-2 tw-overflow-hidden">
|
||||
<span>
|
||||
{{ "centralizeDataOwnershipWarningDesc" | i18n }}
|
||||
</span>
|
||||
<a
|
||||
class="tw-mt-4"
|
||||
bitLink
|
||||
href="https://bitwarden.com/resources/credential-lifecycle-management/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ "centralizeDataOwnershipWarningLink" | i18n }}
|
||||
<i class="bwi bwi-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit, TemplateRef, ViewChild } from "@angular/core";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
OnInit,
|
||||
signal,
|
||||
Signal,
|
||||
TemplateRef,
|
||||
viewChild,
|
||||
WritableSignal,
|
||||
} from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { CenterPositionStrategy, DialogService } from "@bitwarden/components";
|
||||
import { EncString } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
|
||||
import { OrganizationDataOwnershipPolicyDialogComponent } from "../policy-edit-dialogs";
|
||||
|
||||
interface VNextPolicyRequest {
|
||||
export interface VNextPolicyRequest {
|
||||
policy: PolicyRequest;
|
||||
metadata: {
|
||||
defaultUserCollectionName: string;
|
||||
@@ -20,11 +32,17 @@ interface VNextPolicyRequest {
|
||||
}
|
||||
|
||||
export class vNextOrganizationDataOwnershipPolicy extends BasePolicyEditDefinition {
|
||||
name = "organizationDataOwnership";
|
||||
description = "organizationDataOwnershipDesc";
|
||||
name = "centralizeDataOwnership";
|
||||
description = "centralizeDataOwnershipDesc";
|
||||
type = PolicyType.OrganizationDataOwnership;
|
||||
component = vNextOrganizationDataOwnershipPolicyComponent;
|
||||
showDescription = false;
|
||||
|
||||
editDialogComponent = OrganizationDataOwnershipPolicyDialogComponent;
|
||||
|
||||
override display$(organization: Organization, configService: ConfigService): Observable<boolean> {
|
||||
return configService.getFeatureFlag$(FeatureFlag.MigrateMyVaultToMyItems);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -38,27 +56,16 @@ export class vNextOrganizationDataOwnershipPolicyComponent
|
||||
implements OnInit
|
||||
{
|
||||
constructor(
|
||||
private dialogService: DialogService,
|
||||
private i18nService: I18nService,
|
||||
private encryptService: EncryptService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
private readonly policyForm: Signal<TemplateRef<any> | undefined> = viewChild("step0");
|
||||
private readonly warningContent: Signal<TemplateRef<any> | undefined> = viewChild("step1");
|
||||
protected readonly step: WritableSignal<number> = signal(0);
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild("dialog", { static: true }) warningContent!: TemplateRef<unknown>;
|
||||
|
||||
override async confirm(): Promise<boolean> {
|
||||
if (this.policyResponse?.enabled && !this.enabled.value) {
|
||||
const dialogRef = this.dialogService.open(this.warningContent, {
|
||||
positionStrategy: new CenterPositionStrategy(),
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
return Boolean(result);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected steps = [this.policyForm, this.warningContent];
|
||||
|
||||
async buildVNextRequest(orgKey: OrgKey): Promise<VNextPolicyRequest> {
|
||||
if (!this.policy) {
|
||||
@@ -90,4 +97,8 @@ export class vNextOrganizationDataOwnershipPolicyComponent
|
||||
|
||||
return encrypted.encryptedString;
|
||||
}
|
||||
|
||||
setStep(step: number) {
|
||||
this.step.set(step);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
@@ -28,7 +29,7 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "./base-policy-edit.component";
|
||||
import { vNextOrganizationDataOwnershipPolicyComponent } from "./policy-edit-definitions/vnext-organization-data-ownership.component";
|
||||
import { VNextPolicyRequest } from "./policy-edit-definitions/organization-data-ownership.component";
|
||||
|
||||
export type PolicyEditDialogData = {
|
||||
/**
|
||||
@@ -73,13 +74,24 @@ export class PolicyEditDialogComponent implements AfterViewInit {
|
||||
private formBuilder: FormBuilder,
|
||||
protected dialogRef: DialogRef<PolicyEditDialogResult>,
|
||||
protected toastService: ToastService,
|
||||
private keyService: KeyService,
|
||||
protected keyService: KeyService,
|
||||
) {}
|
||||
|
||||
get policy(): BasePolicyEditDefinition {
|
||||
return this.data.policy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if the policy component has the buildVNextRequest method.
|
||||
*/
|
||||
private hasVNextRequest(
|
||||
component: BasePolicyEditComponent,
|
||||
): component is BasePolicyEditComponent & {
|
||||
buildVNextRequest: (orgKey: OrgKey) => Promise<VNextPolicyRequest>;
|
||||
} {
|
||||
return "buildVNextRequest" in component && typeof component.buildVNextRequest === "function";
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates the child policy component and inserts it into the view.
|
||||
*/
|
||||
@@ -129,7 +141,7 @@ export class PolicyEditDialogComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.policyComponent instanceof vNextOrganizationDataOwnershipPolicyComponent) {
|
||||
if (this.hasVNextRequest(this.policyComponent)) {
|
||||
await this.handleVNextSubmission(this.policyComponent);
|
||||
} else {
|
||||
await this.handleStandardSubmission();
|
||||
@@ -158,7 +170,9 @@ export class PolicyEditDialogComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
private async handleVNextSubmission(
|
||||
policyComponent: vNextOrganizationDataOwnershipPolicyComponent,
|
||||
policyComponent: BasePolicyEditComponent & {
|
||||
buildVNextRequest: (orgKey: OrgKey) => Promise<VNextPolicyRequest>;
|
||||
},
|
||||
): Promise<void> {
|
||||
const orgKey = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
@@ -173,12 +187,12 @@ export class PolicyEditDialogComponent implements AfterViewInit {
|
||||
throw new Error("No encryption key for this organization.");
|
||||
}
|
||||
|
||||
const vNextRequest = await policyComponent.buildVNextRequest(orgKey);
|
||||
const request = await policyComponent.buildVNextRequest(orgKey);
|
||||
|
||||
await this.policyApiService.putPolicyVNext(
|
||||
this.data.organizationId,
|
||||
this.data.policy.type,
|
||||
vNextRequest,
|
||||
request,
|
||||
);
|
||||
}
|
||||
static open = (dialogService: DialogService, config: DialogConfig<PolicyEditDialogData>) => {
|
||||
|
||||
@@ -41,20 +41,15 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
import { AutoConfirmPolicyEditComponent } from "./policy-edit-definitions/auto-confirm-policy.component";
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { AutoConfirmPolicyEditComponent } from "../policy-edit-definitions/auto-confirm-policy.component";
|
||||
import {
|
||||
PolicyEditDialogComponent,
|
||||
PolicyEditDialogData,
|
||||
PolicyEditDialogResult,
|
||||
} from "./policy-edit-dialog.component";
|
||||
} from "../policy-edit-dialog.component";
|
||||
|
||||
export type MultiStepSubmit = {
|
||||
sideEffect: () => Promise<void>;
|
||||
footerContent: Signal<TemplateRef<unknown> | undefined>;
|
||||
titleContent: Signal<TemplateRef<unknown> | undefined>;
|
||||
};
|
||||
import { MultiStepSubmit } from "./models";
|
||||
|
||||
export type AutoConfirmPolicyDialogData = PolicyEditDialogData & {
|
||||
firstTimeDialog?: boolean;
|
||||
@@ -202,6 +197,7 @@ export class AutoConfirmPolicyDialogComponent
|
||||
}
|
||||
|
||||
const autoConfirmRequest = await this.policyComponent.buildRequest();
|
||||
|
||||
await this.policyApiService.putPolicy(
|
||||
this.data.organizationId,
|
||||
this.data.policy.type,
|
||||
@@ -235,7 +231,7 @@ export class AutoConfirmPolicyDialogComponent
|
||||
data: null,
|
||||
};
|
||||
|
||||
await this.policyApiService.putPolicy(
|
||||
await this.policyApiService.putPolicyVNext(
|
||||
this.data.organizationId,
|
||||
PolicyType.SingleOrg,
|
||||
singleOrgRequest,
|
||||
@@ -260,7 +256,10 @@ export class AutoConfirmPolicyDialogComponent
|
||||
|
||||
try {
|
||||
const multiStepSubmit = await firstValueFrom(this.multiStepSubmit);
|
||||
await multiStepSubmit[this.currentStep()].sideEffect();
|
||||
const sideEffect = multiStepSubmit[this.currentStep()].sideEffect;
|
||||
if (sideEffect) {
|
||||
await sideEffect();
|
||||
}
|
||||
|
||||
if (this.currentStep() === multiStepSubmit.length - 1) {
|
||||
this.dialogRef.close("saved");
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./auto-confirm-edit-policy-dialog.component";
|
||||
export * from "./organization-data-ownership-edit-policy-dialog.component";
|
||||
export * from "./models";
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Signal, TemplateRef } from "@angular/core";
|
||||
|
||||
export type MultiStepSubmit = {
|
||||
sideEffect?: () => Promise<void>;
|
||||
footerContent: Signal<TemplateRef<unknown> | undefined>;
|
||||
titleContent: Signal<TemplateRef<unknown> | undefined>;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog [loading]="loading">
|
||||
<ng-container bitDialogTitle>
|
||||
@let title = multiStepSubmit()[currentStep()]?.titleContent();
|
||||
@if (title) {
|
||||
<ng-container [ngTemplateOutlet]="title"></ng-container>
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
<ng-container bitDialogContent>
|
||||
@if (loading) {
|
||||
<div>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
}
|
||||
<div [hidden]="loading">
|
||||
@if (policy.showDescription) {
|
||||
<p bitTypography="body1">{{ policy.description | i18n }}</p>
|
||||
}
|
||||
</div>
|
||||
<ng-template #policyForm></ng-template>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
@let footer = multiStepSubmit()[currentStep()]?.footerContent();
|
||||
@if (footer) {
|
||||
<ng-container [ngTemplateOutlet]="footer"></ng-container>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
<ng-template #step0Title>
|
||||
{{ policy.name | i18n }}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #step1Title>
|
||||
{{ "centralizeDataOwnershipWarningTitle" | i18n }}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #step0>
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="saveDisabled$ | async"
|
||||
bitFormButton
|
||||
type="submit"
|
||||
>
|
||||
@if (policyComponent?.policyResponse?.enabled) {
|
||||
{{ "save" | i18n }}
|
||||
} @else {
|
||||
{{ "continue" | i18n }}
|
||||
}
|
||||
</button>
|
||||
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #step1>
|
||||
<button bitButton buttonType="primary" bitFormButton type="submit">
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,224 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Inject,
|
||||
signal,
|
||||
TemplateRef,
|
||||
viewChild,
|
||||
WritableSignal,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
defer,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
startWith,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { vNextOrganizationDataOwnershipPolicyComponent } from "../policy-edit-definitions";
|
||||
import {
|
||||
PolicyEditDialogComponent,
|
||||
PolicyEditDialogData,
|
||||
PolicyEditDialogResult,
|
||||
} from "../policy-edit-dialog.component";
|
||||
|
||||
import { MultiStepSubmit } from "./models";
|
||||
|
||||
/**
|
||||
* Custom policy dialog component for Centralize Organization Data
|
||||
* Ownership policy. Satisfies the PolicyDialogComponent interface
|
||||
* structurally via its static open() function.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "organization-data-ownership-edit-policy-dialog.component.html",
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class OrganizationDataOwnershipPolicyDialogComponent
|
||||
extends PolicyEditDialogComponent
|
||||
implements AfterViewInit
|
||||
{
|
||||
policyType = PolicyType;
|
||||
|
||||
protected centralizeDataOwnershipEnabled$: Observable<boolean> = defer(() =>
|
||||
from(
|
||||
this.policyApiService.getPolicy(
|
||||
this.data.organizationId,
|
||||
PolicyType.OrganizationDataOwnership,
|
||||
),
|
||||
).pipe(
|
||||
map((policy) => policy.enabled),
|
||||
catchError(() => of(false)),
|
||||
),
|
||||
);
|
||||
|
||||
protected readonly currentStep: WritableSignal<number> = signal(0);
|
||||
protected readonly multiStepSubmit: WritableSignal<MultiStepSubmit[]> = signal([]);
|
||||
|
||||
private readonly policyForm = viewChild.required<TemplateRef<unknown>>("step0");
|
||||
private readonly warningContent = viewChild.required<TemplateRef<unknown>>("step1");
|
||||
private readonly policyFormTitle = viewChild.required<TemplateRef<unknown>>("step0Title");
|
||||
private readonly warningTitle = viewChild.required<TemplateRef<unknown>>("step1Title");
|
||||
|
||||
override policyComponent: vNextOrganizationDataOwnershipPolicyComponent | undefined;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: PolicyEditDialogData,
|
||||
accountService: AccountService,
|
||||
policyApiService: PolicyApiServiceAbstraction,
|
||||
i18nService: I18nService,
|
||||
cdr: ChangeDetectorRef,
|
||||
formBuilder: FormBuilder,
|
||||
dialogRef: DialogRef<PolicyEditDialogResult>,
|
||||
toastService: ToastService,
|
||||
protected keyService: KeyService,
|
||||
) {
|
||||
super(
|
||||
data,
|
||||
accountService,
|
||||
policyApiService,
|
||||
i18nService,
|
||||
cdr,
|
||||
formBuilder,
|
||||
dialogRef,
|
||||
toastService,
|
||||
keyService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngAfterViewInit() {
|
||||
await super.ngAfterViewInit();
|
||||
|
||||
if (this.policyComponent) {
|
||||
this.saveDisabled$ = combineLatest([
|
||||
this.centralizeDataOwnershipEnabled$,
|
||||
this.policyComponent.enabled.valueChanges.pipe(
|
||||
startWith(this.policyComponent.enabled.value),
|
||||
),
|
||||
]).pipe(map(([policyEnabled, value]) => !policyEnabled && !value));
|
||||
}
|
||||
|
||||
this.multiStepSubmit.set(this.buildMultiStepSubmit());
|
||||
}
|
||||
|
||||
private buildMultiStepSubmit(): MultiStepSubmit[] {
|
||||
if (this.policyComponent?.policyResponse?.enabled) {
|
||||
return [
|
||||
{
|
||||
sideEffect: () => this.handleSubmit(),
|
||||
footerContent: this.policyForm,
|
||||
titleContent: this.policyFormTitle,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
footerContent: this.policyForm,
|
||||
titleContent: this.policyFormTitle,
|
||||
},
|
||||
{
|
||||
sideEffect: () => this.handleSubmit(),
|
||||
footerContent: this.warningContent,
|
||||
titleContent: this.warningTitle,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private async handleSubmit() {
|
||||
if (!this.policyComponent) {
|
||||
throw new Error("PolicyComponent not initialized.");
|
||||
}
|
||||
|
||||
const orgKey = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
),
|
||||
);
|
||||
|
||||
assertNonNullish(orgKey, "Org key not provided");
|
||||
|
||||
const request = await this.policyComponent.buildVNextRequest(
|
||||
orgKey[this.data.organizationId as OrganizationId],
|
||||
);
|
||||
|
||||
await this.policyApiService.putPolicyVNext(
|
||||
this.data.organizationId,
|
||||
this.data.policy.type,
|
||||
request,
|
||||
);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("editedPolicyId", this.i18nService.t(this.data.policy.name)),
|
||||
});
|
||||
|
||||
if (!this.policyComponent.enabled.value) {
|
||||
this.dialogRef.close("saved");
|
||||
}
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
if (!this.policyComponent) {
|
||||
throw new Error("PolicyComponent not initialized.");
|
||||
}
|
||||
|
||||
if ((await this.policyComponent.confirm()) == false) {
|
||||
this.dialogRef.close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const sideEffect = this.multiStepSubmit()[this.currentStep()].sideEffect;
|
||||
if (sideEffect) {
|
||||
await sideEffect();
|
||||
}
|
||||
|
||||
if (this.currentStep() === this.multiStepSubmit().length - 1) {
|
||||
this.dialogRef.close("saved");
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentStep.update((value) => value + 1);
|
||||
this.policyComponent.setStep(this.currentStep());
|
||||
} catch (error: any) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
static open = (dialogService: DialogService, config: DialogConfig<PolicyEditDialogData>) => {
|
||||
return dialogService.open<PolicyEditDialogResult>(
|
||||
OrganizationDataOwnershipPolicyDialogComponent,
|
||||
config,
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
InitializeJitPasswordCredentials,
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordUserType,
|
||||
@@ -20,11 +21,13 @@ import { AccountCryptographicStateService } from "@bitwarden/common/key-manageme
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { RouterService } from "@bitwarden/web-vault/app/core";
|
||||
@@ -47,6 +50,7 @@ describe("WebSetInitialPasswordService", () => {
|
||||
let organizationInviteService: MockProxy<OrganizationInviteService>;
|
||||
let routerService: MockProxy<RouterService>;
|
||||
let accountCryptographicStateService: MockProxy<AccountCryptographicStateService>;
|
||||
let registerSdkService: MockProxy<RegisterSdkService>;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
@@ -62,6 +66,7 @@ describe("WebSetInitialPasswordService", () => {
|
||||
organizationInviteService = mock<OrganizationInviteService>();
|
||||
routerService = mock<RouterService>();
|
||||
accountCryptographicStateService = mock<AccountCryptographicStateService>();
|
||||
registerSdkService = mock<RegisterSdkService>();
|
||||
|
||||
sut = new WebSetInitialPasswordService(
|
||||
apiService,
|
||||
@@ -77,6 +82,7 @@ describe("WebSetInitialPasswordService", () => {
|
||||
organizationInviteService,
|
||||
routerService,
|
||||
accountCryptographicStateService,
|
||||
registerSdkService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -208,4 +214,36 @@ describe("WebSetInitialPasswordService", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("initializePasswordJitPasswordUserV2Encryption(...)", () => {
|
||||
it("should call routerService.getAndClearLoginRedirectUrl() and organizationInviteService.clearOrganizationInvitation()", async () => {
|
||||
// Arrange
|
||||
const credentials: InitializeJitPasswordCredentials = {
|
||||
newPasswordHint: "newPasswordHint",
|
||||
orgSsoIdentifier: "orgSsoIdentifier",
|
||||
orgId: "orgId" as OrganizationId,
|
||||
resetPasswordAutoEnroll: false,
|
||||
newPassword: "newPassword123!",
|
||||
salt: "user@example.com" as MasterPasswordSalt,
|
||||
};
|
||||
const userId = "userId" as UserId;
|
||||
|
||||
const superSpy = jest
|
||||
.spyOn(
|
||||
Object.getPrototypeOf(Object.getPrototypeOf(sut)),
|
||||
"initializePasswordJitPasswordUserV2Encryption",
|
||||
)
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
await sut.initializePasswordJitPasswordUserV2Encryption(credentials, userId);
|
||||
|
||||
// Assert
|
||||
expect(superSpy).toHaveBeenCalledWith(credentials, userId);
|
||||
expect(routerService.getAndClearLoginRedirectUrl).toHaveBeenCalledTimes(1);
|
||||
expect(organizationInviteService.clearOrganizationInvitation).toHaveBeenCalledTimes(1);
|
||||
|
||||
superSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { DefaultSetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/default-set-initial-password.service.implementation";
|
||||
import {
|
||||
InitializeJitPasswordCredentials,
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordUserType,
|
||||
@@ -14,6 +15,7 @@ import { AccountCryptographicStateService } from "@bitwarden/common/key-manageme
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { RouterService } from "@bitwarden/web-vault/app/core";
|
||||
@@ -36,6 +38,7 @@ export class WebSetInitialPasswordService
|
||||
private organizationInviteService: OrganizationInviteService,
|
||||
private routerService: RouterService,
|
||||
protected accountCryptographicStateService: AccountCryptographicStateService,
|
||||
protected registerSdkService: RegisterSdkService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
@@ -49,6 +52,7 @@ export class WebSetInitialPasswordService
|
||||
organizationUserApiService,
|
||||
userDecryptionOptionsService,
|
||||
accountCryptographicStateService,
|
||||
registerSdkService,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,4 +87,15 @@ export class WebSetInitialPasswordService
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
await this.organizationInviteService.clearOrganizationInvitation();
|
||||
}
|
||||
|
||||
override async initializePasswordJitPasswordUserV2Encryption(
|
||||
credentials: InitializeJitPasswordCredentials,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await super.initializePasswordJitPasswordUserV2Encryption(credentials, userId);
|
||||
|
||||
// TODO: Investigate refactoring the following logic in https://bitwarden.atlassian.net/browse/PM-22615
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
await this.organizationInviteService.clearOrganizationInvitation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import { Router } from "@angular/router";
|
||||
|
||||
import {
|
||||
CollectionAdminService,
|
||||
DefaultCollectionAdminService,
|
||||
OrganizationUserApiService,
|
||||
CollectionService,
|
||||
OrganizationUserService,
|
||||
DefaultCollectionAdminService,
|
||||
DefaultOrganizationUserService,
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { DefaultDeviceManagementComponentService } from "@bitwarden/angular/auth/device-management/default-device-management-component.service";
|
||||
import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction";
|
||||
@@ -27,17 +27,17 @@ import {
|
||||
OBSERVABLE_DISK_LOCAL_STORAGE,
|
||||
OBSERVABLE_DISK_STORAGE,
|
||||
OBSERVABLE_MEMORY_STORAGE,
|
||||
SafeInjectionToken,
|
||||
SECURE_STORAGE,
|
||||
SYSTEM_LANGUAGE,
|
||||
SafeInjectionToken,
|
||||
WINDOW,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import {
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
LoginComponentService,
|
||||
SsoComponentService,
|
||||
LoginDecryptionOptionsService,
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
SsoComponentService,
|
||||
TwoFactorAuthDuoComponentService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
@@ -90,6 +90,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
@@ -120,9 +121,9 @@ import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { GeneratorServicesModule } from "@bitwarden/generator-components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
BiometricsService,
|
||||
KdfConfigService,
|
||||
KeyService as KeyServiceAbstraction,
|
||||
BiometricsService,
|
||||
} from "@bitwarden/key-management";
|
||||
import {
|
||||
LockComponentService,
|
||||
@@ -135,17 +136,17 @@ import { WebVaultPremiumUpgradePromptService } from "@bitwarden/web-vault/app/va
|
||||
|
||||
import { flagEnabled } from "../../utils/flags";
|
||||
import {
|
||||
POLICY_EDIT_REGISTER,
|
||||
ossPolicyEditRegister,
|
||||
POLICY_EDIT_REGISTER,
|
||||
} from "../admin-console/organizations/policies";
|
||||
import {
|
||||
LinkSsoService,
|
||||
WebChangePasswordService,
|
||||
WebRegistrationFinishService,
|
||||
WebLoginComponentService,
|
||||
WebLoginDecryptionOptionsService,
|
||||
WebTwoFactorAuthDuoComponentService,
|
||||
LinkSsoService,
|
||||
WebRegistrationFinishService,
|
||||
WebSetInitialPasswordService,
|
||||
WebTwoFactorAuthDuoComponentService,
|
||||
} from "../auth";
|
||||
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
|
||||
import { WebPremiumInterestStateService } from "../billing/services/premium-interest/web-premium-interest-state.service";
|
||||
@@ -320,6 +321,7 @@ const safeProviders: SafeProvider[] = [
|
||||
OrganizationInviteService,
|
||||
RouterService,
|
||||
AccountCryptographicStateService,
|
||||
RegisterSdkService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -5915,6 +5915,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"centralizeDataOwnership":{
|
||||
"message": "Centralize organization ownership"
|
||||
},
|
||||
"centralizeDataOwnershipDesc":{
|
||||
"message": "All member items will be owned and managed by the organization. Admins and owners are exempt. "
|
||||
},
|
||||
"centralizeDataOwnershipContentAnchor": {
|
||||
"message": "Learn more about centralized ownership",
|
||||
"description": "This will be used as a hyperlink"
|
||||
},
|
||||
"benefits":{
|
||||
"message": "Benefits"
|
||||
},
|
||||
"centralizeDataOwnershipBenefit1":{
|
||||
"message": "Gain full visibility into credential health, including shared and unshared items."
|
||||
},
|
||||
"centralizeDataOwnershipBenefit2":{
|
||||
"message": "Easily transfer items during member offboarding and succession, ensuring there are no access gaps."
|
||||
},
|
||||
"centralizeDataOwnershipBenefit3":{
|
||||
"message": "Give all users a dedicated \"My Items\" space for managing their own logins."
|
||||
},
|
||||
"centralizeDataOwnershipWarningTitle": {
|
||||
"message": "Prompt members to transfer their items"
|
||||
},
|
||||
"centralizeDataOwnershipWarningDesc": {
|
||||
"message": "If members have items in their individual vault, they will be prompted to either transfer them to the organization or leave. If they leave, their access is revoked but can be restored anytime."
|
||||
},
|
||||
"centralizeDataOwnershipWarningLink": {
|
||||
"message": "Learn more about the transfer"
|
||||
},
|
||||
"organizationDataOwnership": {
|
||||
"message": "Enforce organization data ownership"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user