mirror of
https://github.com/bitwarden/browser
synced 2026-03-01 19:11:22 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
@@ -267,6 +267,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
response.masterKey,
|
||||
userId,
|
||||
);
|
||||
await this.setUserKeyAndContinue(userKey, userId, true);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy {
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
await this.onSubmit(this.taxInformation);
|
||||
await this.onSubmit?.(this.taxInformation);
|
||||
this.taxInformationUpdated.emit();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from "@angular/core";
|
||||
import { fromEvent, map, merge, Observable, of, Subscription, switchMap } from "rxjs";
|
||||
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeTypes, Theme } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "../../../services/injection-tokens";
|
||||
@@ -15,7 +15,7 @@ export class AngularThemingService implements AbstractThemingService {
|
||||
* @param window The window that should be watched for system theme changes.
|
||||
* @returns An observable that will track the system theme.
|
||||
*/
|
||||
static createSystemThemeFromWindow(window: Window): Observable<ThemeType> {
|
||||
static createSystemThemeFromWindow(window: Window): Observable<Theme> {
|
||||
return merge(
|
||||
// This observable should always emit at least once, so go and get the current system theme designation
|
||||
of(AngularThemingService.getSystemThemeFromWindow(window)),
|
||||
@@ -23,7 +23,7 @@ export class AngularThemingService implements AbstractThemingService {
|
||||
fromEvent<MediaQueryListEvent>(
|
||||
window.matchMedia("(prefers-color-scheme: dark)"),
|
||||
"change",
|
||||
).pipe(map((event) => (event.matches ? ThemeType.Dark : ThemeType.Light))),
|
||||
).pipe(map((event) => (event.matches ? ThemeTypes.Dark : ThemeTypes.Light))),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,15 +32,15 @@ export class AngularThemingService implements AbstractThemingService {
|
||||
* @param window The window to query for the current theme.
|
||||
* @returns The active system theme.
|
||||
*/
|
||||
static getSystemThemeFromWindow(window: Window): ThemeType {
|
||||
static getSystemThemeFromWindow(window: Window): Theme {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? ThemeType.Dark
|
||||
: ThemeType.Light;
|
||||
? ThemeTypes.Dark
|
||||
: ThemeTypes.Light;
|
||||
}
|
||||
|
||||
readonly theme$ = this.themeStateService.selectedTheme$.pipe(
|
||||
switchMap((configuredTheme) => {
|
||||
if (configuredTheme === ThemeType.System) {
|
||||
if (configuredTheme === ThemeTypes.System) {
|
||||
return this.systemTheme$;
|
||||
}
|
||||
|
||||
@@ -51,16 +51,16 @@ export class AngularThemingService implements AbstractThemingService {
|
||||
constructor(
|
||||
private themeStateService: ThemeStateService,
|
||||
@Inject(SYSTEM_THEME_OBSERVABLE)
|
||||
private systemTheme$: Observable<ThemeType>,
|
||||
private systemTheme$: Observable<Theme>,
|
||||
) {}
|
||||
|
||||
applyThemeChangesTo(document: Document): Subscription {
|
||||
return this.theme$.subscribe((theme) => {
|
||||
document.documentElement.classList.remove(
|
||||
"theme_" + ThemeType.Light,
|
||||
"theme_" + ThemeType.Dark,
|
||||
"theme_" + ThemeType.Nord,
|
||||
"theme_" + ThemeType.SolarizedDark,
|
||||
"theme_" + ThemeTypes.Light,
|
||||
"theme_" + ThemeTypes.Dark,
|
||||
"theme_" + ThemeTypes.Nord,
|
||||
"theme_" + ThemeTypes.SolarizedDark,
|
||||
);
|
||||
document.documentElement.classList.add("theme_" + theme);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Observable, Subscription } from "rxjs";
|
||||
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
/**
|
||||
* A service for managing and observing the current application theme.
|
||||
@@ -9,9 +9,9 @@ import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
export abstract class AbstractThemingService {
|
||||
/**
|
||||
* The effective theme based on the user configured choice and the current system theme if
|
||||
* the configured choice is {@link ThemeType.System}.
|
||||
* the configured choice is {@link ThemeTypes.System}.
|
||||
*/
|
||||
abstract theme$: Observable<ThemeType>;
|
||||
abstract theme$: Observable<Theme>;
|
||||
/**
|
||||
* Listens for effective theme changes and applies changes to the provided document.
|
||||
* @param document The document that should have theme classes applied to it.
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { Message } from "@bitwarden/common/platform/messaging";
|
||||
import { VaultTimeout } from "@bitwarden/common/types/vault-timeout.type";
|
||||
@@ -47,7 +47,7 @@ export const SUPPORTS_SECURE_STORAGE = new SafeInjectionToken<boolean>("SUPPORTS
|
||||
export const LOCALES_DIRECTORY = new SafeInjectionToken<string>("LOCALES_DIRECTORY");
|
||||
export const SYSTEM_LANGUAGE = new SafeInjectionToken<string>("SYSTEM_LANGUAGE");
|
||||
export const LOG_MAC_FAILURES = new SafeInjectionToken<boolean>("LOG_MAC_FAILURES");
|
||||
export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken<Observable<ThemeType>>(
|
||||
export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken<Observable<Theme>>(
|
||||
"SYSTEM_THEME_OBSERVABLE",
|
||||
);
|
||||
export const DEFAULT_VAULT_TIMEOUT = new SafeInjectionToken<VaultTimeout>("DEFAULT_VAULT_TIMEOUT");
|
||||
|
||||
@@ -921,7 +921,13 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: InternalMasterPasswordServiceAbstraction,
|
||||
useClass: MasterPasswordService,
|
||||
deps: [StateProvider, StateServiceAbstraction, KeyGenerationServiceAbstraction, EncryptService],
|
||||
deps: [
|
||||
StateProvider,
|
||||
StateServiceAbstraction,
|
||||
KeyGenerationServiceAbstraction,
|
||||
EncryptService,
|
||||
LogService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MasterPasswordServiceAbstraction,
|
||||
|
||||
@@ -483,6 +483,7 @@ export class LockV2Component implements OnInit, OnDestroy {
|
||||
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
masterPasswordVerificationResponse.masterKey,
|
||||
this.activeAccount.id,
|
||||
);
|
||||
await this.setUserKeyAndContinue(userKey, true);
|
||||
}
|
||||
|
||||
@@ -114,7 +114,10 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||
private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> {
|
||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
if (masterKey) {
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
masterKey,
|
||||
userId,
|
||||
);
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +183,10 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
|
||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
if (masterKey) {
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
masterKey,
|
||||
userId,
|
||||
);
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,7 +496,7 @@ describe("SsoLoginStrategy", () => {
|
||||
|
||||
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
masterKey,
|
||||
undefined,
|
||||
userId,
|
||||
undefined,
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, userId);
|
||||
@@ -552,7 +552,7 @@ describe("SsoLoginStrategy", () => {
|
||||
|
||||
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
masterKey,
|
||||
undefined,
|
||||
userId,
|
||||
undefined,
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, userId);
|
||||
|
||||
@@ -338,7 +338,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
return;
|
||||
}
|
||||
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey, userId);
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
}
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ describe("UserApiLoginStrategy", () => {
|
||||
|
||||
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
masterKey,
|
||||
undefined,
|
||||
userId,
|
||||
undefined,
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, userId);
|
||||
|
||||
@@ -69,7 +69,10 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||
if (response.apiUseKeyConnector) {
|
||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
if (masterKey) {
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
masterKey,
|
||||
userId,
|
||||
);
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ describe("AuthRequestService", () => {
|
||||
);
|
||||
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
mockDecryptedMasterKey,
|
||||
undefined,
|
||||
mockUserId,
|
||||
undefined,
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey, mockUserId);
|
||||
|
||||
@@ -150,7 +150,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||
);
|
||||
|
||||
// Decrypt and set user key in state
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey, userId);
|
||||
|
||||
// Set masterKey + masterKeyHash in state after decryption (in case decryption fails)
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
|
||||
@@ -418,6 +418,7 @@ export class PinService implements PinServiceAbstraction {
|
||||
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
masterKey,
|
||||
userId,
|
||||
encUserKey ? new EncString(encUserKey) : undefined,
|
||||
);
|
||||
|
||||
|
||||
@@ -33,16 +33,16 @@ export abstract class MasterPasswordServiceAbstraction {
|
||||
/**
|
||||
* Decrypts the user key with the provided master key
|
||||
* @param masterKey The user's master key
|
||||
* * @param userId The desired user
|
||||
* @param userKey The user's encrypted symmetric key
|
||||
* @param userId The desired user
|
||||
* @throws If either the MasterKey or UserKey are not resolved, or if the UserKey encryption type
|
||||
* is neither AesCbc256_B64 nor AesCbc256_HmacSha256_B64
|
||||
* @returns The user key
|
||||
*/
|
||||
abstract decryptUserKeyWithMasterKey: (
|
||||
masterKey: MasterKey,
|
||||
userId: string,
|
||||
userKey?: EncString,
|
||||
userId?: string,
|
||||
) => Promise<UserKey>;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,9 +64,9 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
|
||||
|
||||
decryptUserKeyWithMasterKey(
|
||||
masterKey: MasterKey,
|
||||
userId: string,
|
||||
userKey?: EncString,
|
||||
userId?: string,
|
||||
): Promise<UserKey> {
|
||||
return this.mock.decryptUserKeyWithMasterKey(masterKey, userKey, userId);
|
||||
return this.mock.decryptUserKeyWithMasterKey(masterKey, userId, userKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
||||
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
@@ -55,6 +57,7 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
private stateService: StateService,
|
||||
private keyGenerationService: KeyGenerationService,
|
||||
private encryptService: EncryptService,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
masterKey$(userId: UserId): Observable<MasterKey> {
|
||||
@@ -149,10 +152,9 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
|
||||
async decryptUserKeyWithMasterKey(
|
||||
masterKey: MasterKey,
|
||||
userId: UserId,
|
||||
userKey?: EncString,
|
||||
userId?: UserId,
|
||||
): Promise<UserKey> {
|
||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
userKey ??= await this.getMasterKeyEncryptedUserKey(userId);
|
||||
masterKey ??= await firstValueFrom(this.masterKey$(userId));
|
||||
|
||||
@@ -185,6 +187,7 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
}
|
||||
|
||||
if (decUserKey == null) {
|
||||
this.logService.warning("Failed to decrypt user key with master key.");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@ import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class OrganizationBillingMetadataResponse extends BaseResponse {
|
||||
isEligibleForSelfHost: boolean;
|
||||
isManaged: boolean;
|
||||
isOnSecretsManagerStandalone: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.isEligibleForSelfHost = this.getResponseProperty("IsEligibleForSelfHost");
|
||||
this.isManaged = this.getResponseProperty("IsManaged");
|
||||
this.isOnSecretsManagerStandalone = this.getResponseProperty("IsOnSecretsManagerStandalone");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* @deprecated prefer the `ThemeTypes` constants and `Theme` type over unsafe enum types
|
||||
**/
|
||||
export enum ThemeType {
|
||||
System = "system",
|
||||
Light = "light",
|
||||
@@ -5,3 +8,13 @@ export enum ThemeType {
|
||||
Nord = "nord",
|
||||
SolarizedDark = "solarizedDark",
|
||||
}
|
||||
|
||||
export const ThemeTypes = {
|
||||
System: "system",
|
||||
Light: "light",
|
||||
Dark: "dark",
|
||||
Nord: "nord",
|
||||
SolarizedDark: "solarizedDark",
|
||||
} as const;
|
||||
|
||||
export type Theme = (typeof ThemeTypes)[keyof typeof ThemeTypes];
|
||||
|
||||
1
libs/common/src/platform/misc/index.ts
Normal file
1
libs/common/src/platform/misc/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./rxjs-operators";
|
||||
58
libs/common/src/platform/misc/rxjs-operators.spec.ts
Normal file
58
libs/common/src/platform/misc/rxjs-operators.spec.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { getById, getByIds } from "./rxjs-operators";
|
||||
|
||||
describe("custom rxjs operators", () => {
|
||||
describe("getById", () => {
|
||||
it("returns an object with a matching id", async () => {
|
||||
const obs = of([
|
||||
{
|
||||
id: 1,
|
||||
data: "one",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: "two",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
data: "three",
|
||||
},
|
||||
]).pipe(getById(2));
|
||||
|
||||
const result = await firstValueFrom(obs);
|
||||
|
||||
expect(result).toEqual({ id: 2, data: "two" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getByIds", () => {
|
||||
it("returns an array of objects with matching ids", async () => {
|
||||
const obs = of([
|
||||
{
|
||||
id: 1,
|
||||
data: "one",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: "two",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
data: "three",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
data: "four",
|
||||
},
|
||||
]).pipe(getByIds([2, 3]));
|
||||
|
||||
const result = await firstValueFrom(obs);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: 2, data: "two" },
|
||||
{ id: 3, data: "three" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
21
libs/common/src/platform/misc/rxjs-operators.ts
Normal file
21
libs/common/src/platform/misc/rxjs-operators.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { map } from "rxjs";
|
||||
|
||||
/**
|
||||
* An rxjs operator that extracts an object by ID from an array of objects.
|
||||
* @param id The ID of the object to return.
|
||||
* @returns The first object with a matching ID, or undefined if no matching object is present.
|
||||
*/
|
||||
export const getById = <TId, T extends { id: TId }>(id: TId) =>
|
||||
map<T[], T | undefined>((objects) => objects.find((o) => o.id === id));
|
||||
|
||||
/**
|
||||
* An rxjs operator that extracts a subset of objects by their IDs from an array of objects.
|
||||
* @param id The IDs of the objects to return.
|
||||
* @returns An array containing objects with matching IDs, or an empty array if there are no matching objects.
|
||||
*/
|
||||
export const getByIds = <TId, T extends { id: TId }>(ids: TId[]) => {
|
||||
const idSet = new Set(ids);
|
||||
return map<T[], T[]>((objects) => {
|
||||
return objects.filter((o) => idSet.has(o.id));
|
||||
});
|
||||
};
|
||||
@@ -82,6 +82,9 @@ type BaseCipherFormConfig = {
|
||||
|
||||
/** Hides the fields that are only applicable to individuals, useful in the Admin Console where folders aren't applicable */
|
||||
hideIndividualVaultFields?: true;
|
||||
|
||||
/** True when the config is built within the context of the Admin Console */
|
||||
isAdminConsole?: true;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,7 @@ import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
@@ -183,6 +184,12 @@ export default {
|
||||
getClientType: () => ClientType.Browser,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: {
|
||||
activeAccount$: new BehaviorSubject({ email: "test@example.com" }),
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
componentWrapperDecorator(
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
<bit-label>{{ "owner" | i18n }}</bit-label>
|
||||
<bit-select formControlName="organizationId">
|
||||
<bit-option
|
||||
*ngIf="allowPersonalOwnership"
|
||||
*ngIf="showPersonalOwnerOption"
|
||||
[value]="null"
|
||||
[label]="'selfOwnershipLabel' | i18n"
|
||||
[label]="userEmail$ | async"
|
||||
></bit-option>
|
||||
<bit-option
|
||||
*ngFor="let org of config.organizations"
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
|
||||
import { ReactiveFormsModule } from "@angular/forms";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { SelectComponent } from "@bitwarden/components";
|
||||
|
||||
import { CipherFormConfig } from "../../abstractions/cipher-form-config.service";
|
||||
import { CipherFormContainer } from "../../cipher-form-container";
|
||||
@@ -20,6 +24,8 @@ describe("ItemDetailsSectionComponent", () => {
|
||||
let cipherFormProvider: MockProxy<CipherFormContainer>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
|
||||
const activeAccount$ = new BehaviorSubject<{ email: string }>({ email: "test@example.com" });
|
||||
|
||||
beforeEach(async () => {
|
||||
cipherFormProvider = mock<CipherFormContainer>();
|
||||
i18nService = mock<I18nService>();
|
||||
@@ -29,6 +35,7 @@ describe("ItemDetailsSectionComponent", () => {
|
||||
providers: [
|
||||
{ provide: CipherFormContainer, useValue: cipherFormProvider },
|
||||
{ provide: I18nService, useValue: i18nService },
|
||||
{ provide: AccountService, useValue: { activeAccount$ } },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
@@ -207,6 +214,35 @@ describe("ItemDetailsSectionComponent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("showPersonalOwnerOption", () => {
|
||||
it("should show personal ownership when the configuration allows", () => {
|
||||
component.config.mode = "edit";
|
||||
component.config.allowPersonalOwnership = true;
|
||||
component.config.organizations = [{ id: "134-433-22" } as Organization];
|
||||
fixture.detectChanges();
|
||||
|
||||
const select = fixture.debugElement.query(By.directive(SelectComponent));
|
||||
const { value, label } = select.componentInstance.items[0];
|
||||
|
||||
expect(value).toBeNull();
|
||||
expect(label).toBe("test@example.com");
|
||||
});
|
||||
|
||||
it("should show personal ownership when the control is disabled", async () => {
|
||||
component.config.mode = "edit";
|
||||
component.config.allowPersonalOwnership = false;
|
||||
component.config.organizations = [{ id: "134-433-22" } as Organization];
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const select = fixture.debugElement.query(By.directive(SelectComponent));
|
||||
|
||||
const { value, label } = select.componentInstance.items[0];
|
||||
expect(value).toBeNull();
|
||||
expect(label).toBe("test@example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("showOwnership", () => {
|
||||
it("should return true if ownership change is allowed or in edit mode with at least one organization", () => {
|
||||
jest.spyOn(component, "allowOwnershipChange", "get").mockReturnValue(true);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { concatMap, map } from "rxjs";
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -68,6 +69,9 @@ export class ItemDetailsSectionComponent implements OnInit {
|
||||
|
||||
protected showCollectionsControl: boolean;
|
||||
|
||||
/** The email address associated with the active account */
|
||||
protected userEmail$ = this.accountService.activeAccount$.pipe(map((account) => account.email));
|
||||
|
||||
@Input({ required: true })
|
||||
config: CipherFormConfig;
|
||||
|
||||
@@ -96,11 +100,23 @@ export class ItemDetailsSectionComponent implements OnInit {
|
||||
return this.config.initialValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the personal ownership option in the Owner dropdown when:
|
||||
* - Personal ownership is allowed
|
||||
* - The `organizationId` control is disabled. This avoids the scenario
|
||||
* where a the dropdown is empty because the user personally owns the cipher
|
||||
* but cannot edit the ownership.
|
||||
*/
|
||||
get showPersonalOwnerOption() {
|
||||
return this.allowPersonalOwnership || !this.itemDetailsForm.controls.organizationId.enabled;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private cipherFormContainer: CipherFormContainer,
|
||||
private formBuilder: FormBuilder,
|
||||
private i18nService: I18nService,
|
||||
private destroyRef: DestroyRef,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.cipherFormContainer.registerChildForm("itemDetails", this.itemDetailsForm);
|
||||
this.itemDetailsForm.valueChanges
|
||||
@@ -147,9 +163,13 @@ export class ItemDetailsSectionComponent implements OnInit {
|
||||
}
|
||||
|
||||
get showOwnership() {
|
||||
return (
|
||||
this.allowOwnershipChange || (this.organizations.length > 0 && this.config.mode === "edit")
|
||||
);
|
||||
// Show ownership field when editing with available orgs
|
||||
const isEditingWithOrgs = this.organizations.length > 0 && this.config.mode === "edit";
|
||||
|
||||
// When in admin console, ownership should not be shown unless cloning
|
||||
const isAdminConsoleEdit = this.config.isAdminConsole && this.config.mode !== "clone";
|
||||
|
||||
return this.allowOwnershipChange || (isEditingWithOrgs && !isAdminConsoleEdit);
|
||||
}
|
||||
|
||||
get defaultOwner() {
|
||||
|
||||
Reference in New Issue
Block a user