mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 16:53:34 +00:00
[PM-5189] Merging main and fixing conflicts
This commit is contained in:
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -57,6 +57,7 @@ libs/common/src/admin-console @bitwarden/team-admin-console-dev
|
|||||||
libs/admin-console @bitwarden/team-admin-console-dev
|
libs/admin-console @bitwarden/team-admin-console-dev
|
||||||
|
|
||||||
## Billing team files ##
|
## Billing team files ##
|
||||||
|
apps/browser/src/billing @bitwarden/team-billing-dev
|
||||||
apps/web/src/app/billing @bitwarden/team-billing-dev
|
apps/web/src/app/billing @bitwarden/team-billing-dev
|
||||||
libs/angular/src/billing @bitwarden/team-billing-dev
|
libs/angular/src/billing @bitwarden/team-billing-dev
|
||||||
libs/common/src/billing @bitwarden/team-billing-dev
|
libs/common/src/billing @bitwarden/team-billing-dev
|
||||||
|
|||||||
@@ -1120,9 +1120,6 @@
|
|||||||
"commandLockVaultDesc": {
|
"commandLockVaultDesc": {
|
||||||
"message": "Lock the vault"
|
"message": "Lock the vault"
|
||||||
},
|
},
|
||||||
"privateModeWarning": {
|
|
||||||
"message": "Private mode support is experimental and some features are limited."
|
|
||||||
},
|
|
||||||
"customFields": {
|
"customFields": {
|
||||||
"message": "Custom fields"
|
"message": "Custom fields"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -89,7 +89,6 @@
|
|||||||
<p class="text-center" *ngIf="!fido2Data.isFido2Session">
|
<p class="text-center" *ngIf="!fido2Data.isFido2Session">
|
||||||
<button type="button" appStopClick (click)="logOut()">{{ "logOut" | i18n }}</button>
|
<button type="button" appStopClick (click)="logOut()">{{ "logOut" | i18n }}</button>
|
||||||
</p>
|
</p>
|
||||||
<app-private-mode-warning></app-private-mode-warning>
|
|
||||||
<app-callout *ngIf="biometricError" type="error">{{ biometricError }}</app-callout>
|
<app-callout *ngIf="biometricError" type="error">{{ biometricError }}</app-callout>
|
||||||
<p class="text-center text-muted" *ngIf="pendingBiometric">
|
<p class="text-center text-muted" *ngIf="pendingBiometric">
|
||||||
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i> {{ "awaitDesktop" | i18n }}
|
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i> {{ "awaitDesktop" | i18n }}
|
||||||
|
|||||||
@@ -57,7 +57,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-private-mode-warning></app-private-mode-warning>
|
|
||||||
<div class="content login-buttons">
|
<div class="content login-buttons">
|
||||||
<button type="submit" class="btn primary block" [disabled]="form.loading">
|
<button type="submit" class="btn primary block" [disabled]="form.loading">
|
||||||
<span [hidden]="form.loading"
|
<span [hidden]="form.loading"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
|||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||||
import {
|
import {
|
||||||
@@ -75,13 +76,14 @@ describe("AutofillService", () => {
|
|||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
const userVerificationService = mock<UserVerificationService>();
|
const userVerificationService = mock<UserVerificationService>();
|
||||||
const billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
const billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||||
|
const platformUtilsService = mock<PlatformUtilsService>();
|
||||||
let inlineMenuVisibilitySettingMock$!: BehaviorSubject<InlineMenuVisibilitySetting>;
|
let inlineMenuVisibilitySettingMock$!: BehaviorSubject<InlineMenuVisibilitySetting>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService);
|
||||||
inlineMenuVisibilitySettingMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus);
|
inlineMenuVisibilitySettingMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus);
|
||||||
autofillSettingsService = mock<AutofillSettingsServiceAbstraction>();
|
autofillSettingsService = mock<AutofillSettingsServiceAbstraction>();
|
||||||
autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilitySettingMock$;
|
autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilitySettingMock$;
|
||||||
scriptInjectorService = new BrowserScriptInjectorService();
|
|
||||||
autofillService = new AutofillService(
|
autofillService = new AutofillService(
|
||||||
cipherService,
|
cipherService,
|
||||||
autofillSettingsService,
|
autofillSettingsService,
|
||||||
|
|||||||
@@ -110,17 +110,13 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
frameId = 0,
|
frameId = 0,
|
||||||
triggeringOnPageLoad = true,
|
triggeringOnPageLoad = true,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Autofill settings loaded from state can await the active account state indefinitely if
|
// Autofill user settings loaded from state can await the active account state indefinitely
|
||||||
// not guarded by an active account check (e.g. the user is logged in)
|
// if not guarded by an active account check (e.g. the user is logged in)
|
||||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
|
||||||
// These settings are not available until the user logs in
|
|
||||||
let overlayVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off;
|
|
||||||
let autoFillOnPageLoadIsEnabled = false;
|
let autoFillOnPageLoadIsEnabled = false;
|
||||||
|
const overlayVisibility = await this.getInlineMenuVisibility();
|
||||||
|
|
||||||
if (activeAccount) {
|
|
||||||
overlayVisibility = await this.getInlineMenuVisibility();
|
|
||||||
}
|
|
||||||
const mainAutofillScript = overlayVisibility
|
const mainAutofillScript = overlayVisibility
|
||||||
? "bootstrap-autofill-overlay.js"
|
? "bootstrap-autofill-overlay.js"
|
||||||
: "bootstrap-autofill.js";
|
: "bootstrap-autofill.js";
|
||||||
|
|||||||
@@ -106,10 +106,8 @@ import { DefaultConfigService } from "@bitwarden/common/platform/services/config
|
|||||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||||
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
|
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
|
||||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
|
||||||
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||||
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
|
||||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||||
@@ -213,11 +211,14 @@ import { UpdateBadge } from "../platform/listeners/update-badge";
|
|||||||
/* eslint-disable no-restricted-imports */
|
/* eslint-disable no-restricted-imports */
|
||||||
import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender";
|
import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender";
|
||||||
/* eslint-enable no-restricted-imports */
|
/* eslint-enable no-restricted-imports */
|
||||||
|
import { OffscreenDocumentService } from "../platform/offscreen-document/abstractions/offscreen-document";
|
||||||
|
import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service";
|
||||||
import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service";
|
import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service";
|
||||||
import { BrowserCryptoService } from "../platform/services/browser-crypto.service";
|
import { BrowserCryptoService } from "../platform/services/browser-crypto.service";
|
||||||
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
||||||
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
|
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
|
||||||
import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service";
|
import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service";
|
||||||
|
import { BrowserMultithreadEncryptServiceImplementation } from "../platform/services/browser-multithread-encrypt.service.implementation";
|
||||||
import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service";
|
import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service";
|
||||||
import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service";
|
import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service";
|
||||||
import I18nService from "../platform/services/i18n.service";
|
import I18nService from "../platform/services/i18n.service";
|
||||||
@@ -337,6 +338,7 @@ export default class MainBackground {
|
|||||||
userAutoUnlockKeyService: UserAutoUnlockKeyService;
|
userAutoUnlockKeyService: UserAutoUnlockKeyService;
|
||||||
scriptInjectorService: BrowserScriptInjectorService;
|
scriptInjectorService: BrowserScriptInjectorService;
|
||||||
kdfConfigService: kdfConfigServiceAbstraction;
|
kdfConfigService: kdfConfigServiceAbstraction;
|
||||||
|
offscreenDocumentService: OffscreenDocumentService;
|
||||||
|
|
||||||
onUpdatedRan: boolean;
|
onUpdatedRan: boolean;
|
||||||
onReplacedRan: boolean;
|
onReplacedRan: boolean;
|
||||||
@@ -356,10 +358,7 @@ export default class MainBackground {
|
|||||||
private isSafari: boolean;
|
private isSafari: boolean;
|
||||||
private nativeMessagingBackground: NativeMessagingBackground;
|
private nativeMessagingBackground: NativeMessagingBackground;
|
||||||
|
|
||||||
constructor(
|
constructor(public popupOnlyContext: boolean = false) {
|
||||||
public isPrivateMode: boolean = false,
|
|
||||||
public popupOnlyContext: boolean = false,
|
|
||||||
) {
|
|
||||||
// Services
|
// Services
|
||||||
const lockedCallback = async (userId?: string) => {
|
const lockedCallback = async (userId?: string) => {
|
||||||
if (this.notificationsService != null) {
|
if (this.notificationsService != null) {
|
||||||
@@ -397,11 +396,14 @@ export default class MainBackground {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.offscreenDocumentService = new DefaultOffscreenDocumentService();
|
||||||
|
|
||||||
this.platformUtilsService = new BackgroundPlatformUtilsService(
|
this.platformUtilsService = new BackgroundPlatformUtilsService(
|
||||||
this.messagingService,
|
this.messagingService,
|
||||||
(clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs),
|
(clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs),
|
||||||
async () => this.biometricUnlock(),
|
async () => this.biometricUnlock(),
|
||||||
self,
|
self,
|
||||||
|
this.offscreenDocumentService,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Creates a session key for mv3 storage of large memory items
|
// Creates a session key for mv3 storage of large memory items
|
||||||
@@ -443,10 +445,14 @@ export default class MainBackground {
|
|||||||
this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used
|
this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used
|
||||||
this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3)
|
this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3)
|
||||||
? new BrowserMemoryStorageService() // mv3 stores to storage.session
|
? new BrowserMemoryStorageService() // mv3 stores to storage.session
|
||||||
: new BackgroundMemoryStorageService(); // mv2 stores to memory
|
: popupOnlyContext
|
||||||
|
? new ForegroundMemoryStorageService()
|
||||||
|
: new BackgroundMemoryStorageService(); // mv2 stores to memory
|
||||||
this.memoryStorageService = BrowserApi.isManifestVersion(3)
|
this.memoryStorageService = BrowserApi.isManifestVersion(3)
|
||||||
? this.memoryStorageForStateProviders // manifest v3 can reuse the same storage. They are split for v2 due to lacking a good sync mechanism, which isn't true for v3
|
? this.memoryStorageForStateProviders // manifest v3 can reuse the same storage. They are split for v2 due to lacking a good sync mechanism, which isn't true for v3
|
||||||
: new MemoryStorageService();
|
: popupOnlyContext
|
||||||
|
? new ForegroundMemoryStorageService()
|
||||||
|
: new BackgroundMemoryStorageService();
|
||||||
this.largeObjectMemoryStorageForStateProviders = BrowserApi.isManifestVersion(3)
|
this.largeObjectMemoryStorageForStateProviders = BrowserApi.isManifestVersion(3)
|
||||||
? mv3MemoryStorageCreator() // mv3 stores to local-backed session storage
|
? mv3MemoryStorageCreator() // mv3 stores to local-backed session storage
|
||||||
: this.memoryStorageForStateProviders; // mv2 stores to the same location
|
: this.memoryStorageForStateProviders; // mv2 stores to the same location
|
||||||
@@ -469,14 +475,14 @@ export default class MainBackground {
|
|||||||
storageServiceProvider,
|
storageServiceProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.encryptService =
|
this.encryptService = flagEnabled("multithreadDecryption")
|
||||||
flagEnabled("multithreadDecryption") && BrowserApi.isManifestVersion(2)
|
? new BrowserMultithreadEncryptServiceImplementation(
|
||||||
? new MultithreadEncryptServiceImplementation(
|
this.cryptoFunctionService,
|
||||||
this.cryptoFunctionService,
|
this.logService,
|
||||||
this.logService,
|
true,
|
||||||
true,
|
this.offscreenDocumentService,
|
||||||
)
|
)
|
||||||
: new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true);
|
: new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true);
|
||||||
|
|
||||||
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
|
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
|
||||||
storageServiceProvider,
|
storageServiceProvider,
|
||||||
@@ -737,7 +743,6 @@ export default class MainBackground {
|
|||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.folderService,
|
this.folderService,
|
||||||
this.collectionService,
|
this.collectionService,
|
||||||
this.cryptoService,
|
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
this.messagingService,
|
this.messagingService,
|
||||||
this.searchService,
|
this.searchService,
|
||||||
@@ -808,7 +813,10 @@ export default class MainBackground {
|
|||||||
);
|
);
|
||||||
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
|
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
|
||||||
|
|
||||||
this.scriptInjectorService = new BrowserScriptInjectorService();
|
this.scriptInjectorService = new BrowserScriptInjectorService(
|
||||||
|
this.platformUtilsService,
|
||||||
|
this.logService,
|
||||||
|
);
|
||||||
this.autofillService = new AutofillService(
|
this.autofillService = new AutofillService(
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.autofillSettingsService,
|
this.autofillSettingsService,
|
||||||
@@ -1110,27 +1118,9 @@ export default class MainBackground {
|
|||||||
await this.idleBackground.init();
|
await this.idleBackground.init();
|
||||||
this.webRequestBackground?.startListening();
|
this.webRequestBackground?.startListening();
|
||||||
|
|
||||||
if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) {
|
|
||||||
// Set Private Mode windows to the default icon - they do not share state with the background page
|
|
||||||
const privateWindows = await BrowserApi.getPrivateModeWindows();
|
|
||||||
privateWindows.forEach(async (win) => {
|
|
||||||
await new UpdateBadge(self).setBadgeIcon("", win.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
BrowserApi.onWindowCreated(async (win) => {
|
|
||||||
if (win.incognito) {
|
|
||||||
await new UpdateBadge(self).setBadgeIcon("", win.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (!this.isPrivateMode) {
|
await this.refreshBadge();
|
||||||
await this.refreshBadge();
|
|
||||||
}
|
|
||||||
await this.fullSync(true);
|
await this.fullSync(true);
|
||||||
setTimeout(() => this.notificationsService.init(), 2500);
|
setTimeout(() => this.notificationsService.init(), 2500);
|
||||||
resolve();
|
resolve();
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ import {
|
|||||||
internalMasterPasswordServiceFactory,
|
internalMasterPasswordServiceFactory,
|
||||||
MasterPasswordServiceInitOptions,
|
MasterPasswordServiceInitOptions,
|
||||||
} from "../../auth/background/service-factories/master-password-service.factory";
|
} from "../../auth/background/service-factories/master-password-service.factory";
|
||||||
import {
|
|
||||||
CryptoServiceInitOptions,
|
|
||||||
cryptoServiceFactory,
|
|
||||||
} from "../../platform/background/service-factories/crypto-service.factory";
|
|
||||||
import {
|
import {
|
||||||
CachedServices,
|
CachedServices,
|
||||||
factory,
|
factory,
|
||||||
@@ -70,7 +66,6 @@ export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions &
|
|||||||
CipherServiceInitOptions &
|
CipherServiceInitOptions &
|
||||||
FolderServiceInitOptions &
|
FolderServiceInitOptions &
|
||||||
CollectionServiceInitOptions &
|
CollectionServiceInitOptions &
|
||||||
CryptoServiceInitOptions &
|
|
||||||
PlatformUtilsServiceInitOptions &
|
PlatformUtilsServiceInitOptions &
|
||||||
MessagingServiceInitOptions &
|
MessagingServiceInitOptions &
|
||||||
SearchServiceInitOptions &
|
SearchServiceInitOptions &
|
||||||
@@ -94,7 +89,6 @@ export function vaultTimeoutServiceFactory(
|
|||||||
await cipherServiceFactory(cache, opts),
|
await cipherServiceFactory(cache, opts),
|
||||||
await folderServiceFactory(cache, opts),
|
await folderServiceFactory(cache, opts),
|
||||||
await collectionServiceFactory(cache, opts),
|
await collectionServiceFactory(cache, opts),
|
||||||
await cryptoServiceFactory(cache, opts),
|
|
||||||
await platformUtilsServiceFactory(cache, opts),
|
await platformUtilsServiceFactory(cache, opts),
|
||||||
await messagingServiceFactory(cache, opts),
|
await messagingServiceFactory(cache, opts),
|
||||||
await searchServiceFactory(cache, opts),
|
await searchServiceFactory(cache, opts),
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
"default_popup": "popup/index.html"
|
"default_popup": "popup/index.html"
|
||||||
},
|
},
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"<all_urls>",
|
"activeTab",
|
||||||
"tabs",
|
"tabs",
|
||||||
"contextMenus",
|
"contextMenus",
|
||||||
"storage",
|
"storage",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"webNavigation"
|
"webNavigation"
|
||||||
],
|
],
|
||||||
"optional_permissions": ["nativeMessaging", "privacy"],
|
"optional_permissions": ["nativeMessaging", "privacy"],
|
||||||
"host_permissions": ["<all_urls>"],
|
"host_permissions": ["https://*/*", "http://*/*"],
|
||||||
"content_security_policy": {
|
"content_security_policy": {
|
||||||
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",
|
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",
|
||||||
"sandbox": "sandbox allow-scripts; script-src 'self'"
|
"sandbox": "sandbox allow-scripts; script-src 'self'"
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
|
import {
|
||||||
|
LogServiceInitOptions,
|
||||||
|
logServiceFactory,
|
||||||
|
} from "../../background/service-factories/log-service.factory";
|
||||||
import { BrowserScriptInjectorService } from "../../services/browser-script-injector.service";
|
import { BrowserScriptInjectorService } from "../../services/browser-script-injector.service";
|
||||||
|
|
||||||
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
||||||
|
import {
|
||||||
|
PlatformUtilsServiceInitOptions,
|
||||||
|
platformUtilsServiceFactory,
|
||||||
|
} from "./platform-utils-service.factory";
|
||||||
|
|
||||||
type BrowserScriptInjectorServiceOptions = FactoryOptions;
|
type BrowserScriptInjectorServiceOptions = FactoryOptions;
|
||||||
|
|
||||||
export type BrowserScriptInjectorServiceInitOptions = BrowserScriptInjectorServiceOptions;
|
export type BrowserScriptInjectorServiceInitOptions = BrowserScriptInjectorServiceOptions &
|
||||||
|
PlatformUtilsServiceInitOptions &
|
||||||
|
LogServiceInitOptions;
|
||||||
|
|
||||||
export function browserScriptInjectorServiceFactory(
|
export function browserScriptInjectorServiceFactory(
|
||||||
cache: { browserScriptInjectorService?: BrowserScriptInjectorService } & CachedServices,
|
cache: { browserScriptInjectorService?: BrowserScriptInjectorService } & CachedServices,
|
||||||
@@ -14,6 +24,10 @@ export function browserScriptInjectorServiceFactory(
|
|||||||
cache,
|
cache,
|
||||||
"browserScriptInjectorService",
|
"browserScriptInjectorService",
|
||||||
opts,
|
opts,
|
||||||
async () => new BrowserScriptInjectorService(),
|
async () =>
|
||||||
|
new BrowserScriptInjectorService(
|
||||||
|
await platformUtilsServiceFactory(cache, opts),
|
||||||
|
await logServiceFactory(cache, opts),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function platformUtilsServiceFactory(
|
|||||||
opts.platformUtilsServiceOptions.clipboardWriteCallback,
|
opts.platformUtilsServiceOptions.clipboardWriteCallback,
|
||||||
opts.platformUtilsServiceOptions.biometricCallback,
|
opts.platformUtilsServiceOptions.biometricCallback,
|
||||||
opts.platformUtilsServiceOptions.win,
|
opts.platformUtilsServiceOptions.win,
|
||||||
|
null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -525,32 +525,6 @@ describe("BrowserApi", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createOffscreenDocument", () => {
|
|
||||||
it("creates the offscreen document with the supplied reasons and justification", async () => {
|
|
||||||
const reasons = [chrome.offscreen.Reason.CLIPBOARD];
|
|
||||||
const justification = "justification";
|
|
||||||
|
|
||||||
await BrowserApi.createOffscreenDocument(reasons, justification);
|
|
||||||
|
|
||||||
expect(chrome.offscreen.createDocument).toHaveBeenCalledWith({
|
|
||||||
url: "offscreen-document/index.html",
|
|
||||||
reasons,
|
|
||||||
justification,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("closeOffscreenDocument", () => {
|
|
||||||
it("closes the offscreen document", () => {
|
|
||||||
const callbackMock = jest.fn();
|
|
||||||
|
|
||||||
BrowserApi.closeOffscreenDocument(callbackMock);
|
|
||||||
|
|
||||||
expect(chrome.offscreen.closeDocument).toHaveBeenCalled();
|
|
||||||
expect(callbackMock).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("registerContentScriptsMv2", () => {
|
describe("registerContentScriptsMv2", () => {
|
||||||
const details: browser.contentScripts.RegisteredContentScriptOptions = {
|
const details: browser.contentScripts.RegisteredContentScriptOptions = {
|
||||||
matches: ["<all_urls>"],
|
matches: ["<all_urls>"],
|
||||||
|
|||||||
@@ -204,10 +204,6 @@ export class BrowserApi {
|
|||||||
chrome.tabs.sendMessage<TabMessage, T>(tabId, message, options, responseCallback);
|
chrome.tabs.sendMessage<TabMessage, T>(tabId, message, options, responseCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getPrivateModeWindows(): Promise<browser.windows.Window[]> {
|
|
||||||
return (await browser.windows.getAll()).filter((win) => win.incognito);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async onWindowCreated(callback: (win: chrome.windows.Window) => any) {
|
static async onWindowCreated(callback: (win: chrome.windows.Window) => any) {
|
||||||
// FIXME: Make sure that is does not cause a memory leak in Safari or use BrowserApi.AddListener
|
// FIXME: Make sure that is does not cause a memory leak in Safari or use BrowserApi.AddListener
|
||||||
// and test that it doesn't break.
|
// and test that it doesn't break.
|
||||||
@@ -574,34 +570,6 @@ export class BrowserApi {
|
|||||||
chrome.privacy.services.passwordSavingEnabled.set({ value });
|
chrome.privacy.services.passwordSavingEnabled.set({ value });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the offscreen document with the given reasons and justification.
|
|
||||||
*
|
|
||||||
* @param reasons - List of reasons for opening the offscreen document.
|
|
||||||
* @see https://developer.chrome.com/docs/extensions/reference/api/offscreen#type-Reason
|
|
||||||
* @param justification - Custom written justification for opening the offscreen document.
|
|
||||||
*/
|
|
||||||
static async createOffscreenDocument(reasons: chrome.offscreen.Reason[], justification: string) {
|
|
||||||
await chrome.offscreen.createDocument({
|
|
||||||
url: "offscreen-document/index.html",
|
|
||||||
reasons,
|
|
||||||
justification,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the offscreen document.
|
|
||||||
*
|
|
||||||
* @param callback - Optional callback to execute after the offscreen document is closed.
|
|
||||||
*/
|
|
||||||
static closeOffscreenDocument(callback?: () => void) {
|
|
||||||
chrome.offscreen.closeDocument(() => {
|
|
||||||
if (callback) {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles registration of static content scripts within manifest v2.
|
* Handles registration of static content scripts within manifest v2.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
type OffscreenDocumentExtensionMessage = {
|
export type OffscreenDocumentExtensionMessage = {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
command: string;
|
command: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
decryptRequest?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OffscreenExtensionMessageEventParams = {
|
type OffscreenExtensionMessageEventParams = {
|
||||||
@@ -9,18 +10,21 @@ type OffscreenExtensionMessageEventParams = {
|
|||||||
sender: chrome.runtime.MessageSender;
|
sender: chrome.runtime.MessageSender;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OffscreenDocumentExtensionMessageHandlers = {
|
export type OffscreenDocumentExtensionMessageHandlers = {
|
||||||
[key: string]: ({ message, sender }: OffscreenExtensionMessageEventParams) => any;
|
[key: string]: ({ message, sender }: OffscreenExtensionMessageEventParams) => any;
|
||||||
offscreenCopyToClipboard: ({ message }: OffscreenExtensionMessageEventParams) => any;
|
offscreenCopyToClipboard: ({ message }: OffscreenExtensionMessageEventParams) => any;
|
||||||
offscreenReadFromClipboard: () => any;
|
offscreenReadFromClipboard: () => any;
|
||||||
|
offscreenDecryptItems: ({ message }: OffscreenExtensionMessageEventParams) => Promise<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface OffscreenDocument {
|
export interface OffscreenDocument {
|
||||||
init(): void;
|
init(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export abstract class OffscreenDocumentService {
|
||||||
OffscreenDocumentExtensionMessage,
|
abstract withDocument<T>(
|
||||||
OffscreenDocumentExtensionMessageHandlers,
|
reasons: chrome.offscreen.Reason[],
|
||||||
OffscreenDocument,
|
justification: string,
|
||||||
};
|
callback: () => Promise<T> | T,
|
||||||
|
): Promise<T>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { DefaultOffscreenDocumentService } from "./offscreen-document.service";
|
||||||
|
|
||||||
|
class TestCase {
|
||||||
|
synchronicity: string;
|
||||||
|
private _callback: () => Promise<any> | any;
|
||||||
|
get callback() {
|
||||||
|
return jest.fn(this._callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(synchronicity: string, callback: () => Promise<any> | any) {
|
||||||
|
this.synchronicity = synchronicity;
|
||||||
|
this._callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return this.synchronicity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
new TestCase("synchronous callback", () => 42),
|
||||||
|
new TestCase("asynchronous callback", () => Promise.resolve(42)),
|
||||||
|
])("DefaultOffscreenDocumentService %s", (testCase) => {
|
||||||
|
let sut: DefaultOffscreenDocumentService;
|
||||||
|
const reasons = [chrome.offscreen.Reason.TESTING];
|
||||||
|
const justification = "justification is testing";
|
||||||
|
const url = "offscreen-document/index.html";
|
||||||
|
const api = {
|
||||||
|
createDocument: jest.fn(),
|
||||||
|
closeDocument: jest.fn(),
|
||||||
|
hasDocument: jest.fn().mockResolvedValue(false),
|
||||||
|
Reason: chrome.offscreen.Reason,
|
||||||
|
};
|
||||||
|
let callback: jest.Mock<() => Promise<number> | number>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
callback = testCase.callback;
|
||||||
|
chrome.offscreen = api;
|
||||||
|
|
||||||
|
sut = new DefaultOffscreenDocumentService();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("withDocument", () => {
|
||||||
|
it("creates a document when none exists", async () => {
|
||||||
|
await sut.withDocument(reasons, justification, () => {});
|
||||||
|
|
||||||
|
expect(chrome.offscreen.createDocument).toHaveBeenCalledWith({
|
||||||
|
url,
|
||||||
|
reasons,
|
||||||
|
justification,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not create a document when one exists", async () => {
|
||||||
|
api.hasDocument.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await sut.withDocument(reasons, justification, callback);
|
||||||
|
|
||||||
|
expect(chrome.offscreen.createDocument).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each([true, false])("hasDocument returns %s", (hasDocument) => {
|
||||||
|
beforeEach(() => {
|
||||||
|
api.hasDocument.mockResolvedValue(hasDocument);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls the callback", async () => {
|
||||||
|
await sut.withDocument(reasons, justification, callback);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the callback result", async () => {
|
||||||
|
const result = await sut.withDocument(reasons, justification, callback);
|
||||||
|
|
||||||
|
expect(result).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closes the document when the callback completes and no other callbacks are running", async () => {
|
||||||
|
await sut.withDocument(reasons, justification, callback);
|
||||||
|
|
||||||
|
expect(chrome.offscreen.closeDocument).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not close the document when the callback completes and other callbacks are running", async () => {
|
||||||
|
await Promise.all([
|
||||||
|
sut.withDocument(reasons, justification, callback),
|
||||||
|
sut.withDocument(reasons, justification, callback),
|
||||||
|
sut.withDocument(reasons, justification, callback),
|
||||||
|
sut.withDocument(reasons, justification, callback),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(chrome.offscreen.closeDocument).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
export class DefaultOffscreenDocumentService implements DefaultOffscreenDocumentService {
|
||||||
|
private workerCount = 0;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
async withDocument<T>(
|
||||||
|
reasons: chrome.offscreen.Reason[],
|
||||||
|
justification: string,
|
||||||
|
callback: () => Promise<T> | T,
|
||||||
|
): Promise<T> {
|
||||||
|
this.workerCount++;
|
||||||
|
try {
|
||||||
|
if (!(await this.documentExists())) {
|
||||||
|
await this.create(reasons, justification);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await callback();
|
||||||
|
} finally {
|
||||||
|
this.workerCount--;
|
||||||
|
if (this.workerCount === 0) {
|
||||||
|
await this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async create(reasons: chrome.offscreen.Reason[], justification: string): Promise<void> {
|
||||||
|
await chrome.offscreen.createDocument({
|
||||||
|
url: "offscreen-document/index.html",
|
||||||
|
reasons,
|
||||||
|
justification,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async close(): Promise<void> {
|
||||||
|
await chrome.offscreen.closeDocument();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async documentExists(): Promise<boolean> {
|
||||||
|
return await chrome.offscreen.hasDocument();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,25 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||||
|
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
|
||||||
import { flushPromises, sendMockExtensionMessage } from "../../autofill/spec/testing-utils";
|
import { flushPromises, sendMockExtensionMessage } from "../../autofill/spec/testing-utils";
|
||||||
import { BrowserApi } from "../browser/browser-api";
|
import { BrowserApi } from "../browser/browser-api";
|
||||||
import BrowserClipboardService from "../services/browser-clipboard.service";
|
import BrowserClipboardService from "../services/browser-clipboard.service";
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
"@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation",
|
||||||
|
() => ({
|
||||||
|
MultithreadEncryptServiceImplementation: class MultithreadEncryptServiceImplementation {
|
||||||
|
getDecryptedItemsFromWorker = async <T extends InitializerMetadata>(
|
||||||
|
items: Decryptable<T>[],
|
||||||
|
_key: SymmetricCryptoKey,
|
||||||
|
): Promise<string> => JSON.stringify(items);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
describe("OffscreenDocument", () => {
|
describe("OffscreenDocument", () => {
|
||||||
const browserApiMessageListenerSpy = jest.spyOn(BrowserApi, "messageListener");
|
const browserApiMessageListenerSpy = jest.spyOn(BrowserApi, "messageListener");
|
||||||
const browserClipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy");
|
const browserClipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy");
|
||||||
@@ -60,5 +78,37 @@ describe("OffscreenDocument", () => {
|
|||||||
expect(browserClipboardServiceReadSpy).toHaveBeenCalledWith(window);
|
expect(browserClipboardServiceReadSpy).toHaveBeenCalledWith(window);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("handleOffscreenDecryptItems", () => {
|
||||||
|
it("returns an empty array as a string if the decrypt request is not present in the message", async () => {
|
||||||
|
let response: string | undefined;
|
||||||
|
sendMockExtensionMessage(
|
||||||
|
{ command: "offscreenDecryptItems" },
|
||||||
|
mock<chrome.runtime.MessageSender>(),
|
||||||
|
(res: string) => (response = res),
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(response).toBe("[]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decrypts the items and sends back the response as a string", async () => {
|
||||||
|
const items = [{ id: "test" }];
|
||||||
|
const key = { id: "test" };
|
||||||
|
const decryptRequest = JSON.stringify({ items, key });
|
||||||
|
let response: string | undefined;
|
||||||
|
|
||||||
|
sendMockExtensionMessage(
|
||||||
|
{ command: "offscreenDecryptItems", decryptRequest },
|
||||||
|
mock<chrome.runtime.MessageSender>(),
|
||||||
|
(res: string) => {
|
||||||
|
response = res;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(response).toBe(JSON.stringify(items));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,35 @@
|
|||||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||||
|
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||||
|
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||||
|
|
||||||
import { BrowserApi } from "../browser/browser-api";
|
import { BrowserApi } from "../browser/browser-api";
|
||||||
import BrowserClipboardService from "../services/browser-clipboard.service";
|
import BrowserClipboardService from "../services/browser-clipboard.service";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
OffscreenDocument as OffscreenDocumentInterface,
|
||||||
OffscreenDocumentExtensionMessage,
|
OffscreenDocumentExtensionMessage,
|
||||||
OffscreenDocumentExtensionMessageHandlers,
|
OffscreenDocumentExtensionMessageHandlers,
|
||||||
OffscreenDocument as OffscreenDocumentInterface,
|
|
||||||
} from "./abstractions/offscreen-document";
|
} from "./abstractions/offscreen-document";
|
||||||
|
|
||||||
class OffscreenDocument implements OffscreenDocumentInterface {
|
class OffscreenDocument implements OffscreenDocumentInterface {
|
||||||
private consoleLogService: ConsoleLogService = new ConsoleLogService(false);
|
private readonly consoleLogService: ConsoleLogService;
|
||||||
|
private encryptService: MultithreadEncryptServiceImplementation;
|
||||||
private readonly extensionMessageHandlers: OffscreenDocumentExtensionMessageHandlers = {
|
private readonly extensionMessageHandlers: OffscreenDocumentExtensionMessageHandlers = {
|
||||||
offscreenCopyToClipboard: ({ message }) => this.handleOffscreenCopyToClipboard(message),
|
offscreenCopyToClipboard: ({ message }) => this.handleOffscreenCopyToClipboard(message),
|
||||||
offscreenReadFromClipboard: () => this.handleOffscreenReadFromClipboard(),
|
offscreenReadFromClipboard: () => this.handleOffscreenReadFromClipboard(),
|
||||||
|
offscreenDecryptItems: ({ message }) => this.handleOffscreenDecryptItems(message),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const cryptoFunctionService = new WebCryptoFunctionService(self);
|
||||||
|
this.consoleLogService = new ConsoleLogService(false);
|
||||||
|
this.encryptService = new MultithreadEncryptServiceImplementation(
|
||||||
|
cryptoFunctionService,
|
||||||
|
this.consoleLogService,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the offscreen document extension.
|
* Initializes the offscreen document extension.
|
||||||
*/
|
*/
|
||||||
@@ -39,6 +53,23 @@ class OffscreenDocument implements OffscreenDocumentInterface {
|
|||||||
return await BrowserClipboardService.read(self);
|
return await BrowserClipboardService.read(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the items in the message using the encrypt service.
|
||||||
|
*
|
||||||
|
* @param message - The extension message containing the items to decrypt
|
||||||
|
*/
|
||||||
|
private async handleOffscreenDecryptItems(
|
||||||
|
message: OffscreenDocumentExtensionMessage,
|
||||||
|
): Promise<string> {
|
||||||
|
const { decryptRequest } = message;
|
||||||
|
if (!decryptRequest) {
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = JSON.parse(decryptRequest);
|
||||||
|
return await this.encryptService.getDecryptedItemsFromWorker(request.items, request.key);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the listener for extension messages.
|
* Sets up the listener for extension messages.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -138,28 +138,6 @@ describe("BrowserPopupUtils", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("inPrivateMode", () => {
|
|
||||||
it("returns false if the background requires initialization", () => {
|
|
||||||
jest.spyOn(BrowserPopupUtils, "backgroundInitializationRequired").mockReturnValue(false);
|
|
||||||
|
|
||||||
expect(BrowserPopupUtils.inPrivateMode()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false if the manifest version is for version 3", () => {
|
|
||||||
jest.spyOn(BrowserPopupUtils, "backgroundInitializationRequired").mockReturnValue(true);
|
|
||||||
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
|
||||||
|
|
||||||
expect(BrowserPopupUtils.inPrivateMode()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true if the background does not require initalization and the manifest version is version 2", () => {
|
|
||||||
jest.spyOn(BrowserPopupUtils, "backgroundInitializationRequired").mockReturnValue(true);
|
|
||||||
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(2);
|
|
||||||
|
|
||||||
expect(BrowserPopupUtils.inPrivateMode()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("openPopout", () => {
|
describe("openPopout", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(BrowserApi, "getWindow").mockResolvedValueOnce({
|
jest.spyOn(BrowserApi, "getWindow").mockResolvedValueOnce({
|
||||||
|
|||||||
@@ -89,13 +89,6 @@ class BrowserPopupUtils {
|
|||||||
return !BrowserApi.getBackgroundPage();
|
return !BrowserApi.getBackgroundPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Identifies if the popup is loading in private mode.
|
|
||||||
*/
|
|
||||||
static inPrivateMode() {
|
|
||||||
return BrowserPopupUtils.backgroundInitializationRequired() && !BrowserApi.isManifestVersion(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a popout window of any extension page. If the popout window is already open, it will be focused.
|
* Opens a popout window of any extension page. If the popout window is already open, it will be focused.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||||
|
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||||
|
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { InitializerKey } from "@bitwarden/common/platform/services/cryptography/initializer-key";
|
||||||
|
import { makeStaticByteArray } from "@bitwarden/common/spec";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../browser/browser-api";
|
||||||
|
import { OffscreenDocumentService } from "../offscreen-document/abstractions/offscreen-document";
|
||||||
|
|
||||||
|
import { BrowserMultithreadEncryptServiceImplementation } from "./browser-multithread-encrypt.service.implementation";
|
||||||
|
|
||||||
|
describe("BrowserMultithreadEncryptServiceImplementation", () => {
|
||||||
|
let cryptoFunctionServiceMock: MockProxy<CryptoFunctionService>;
|
||||||
|
let logServiceMock: MockProxy<LogService>;
|
||||||
|
let offscreenDocumentServiceMock: MockProxy<OffscreenDocumentService>;
|
||||||
|
let encryptService: BrowserMultithreadEncryptServiceImplementation;
|
||||||
|
const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
|
||||||
|
const sendMessageWithResponseSpy = jest.spyOn(BrowserApi, "sendMessageWithResponse");
|
||||||
|
const encType = EncryptionType.AesCbc256_HmacSha256_B64;
|
||||||
|
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 100), encType);
|
||||||
|
const items: Decryptable<InitializerMetadata>[] = [
|
||||||
|
{
|
||||||
|
decrypt: jest.fn(),
|
||||||
|
initializerKey: InitializerKey.Cipher,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cryptoFunctionServiceMock = mock<CryptoFunctionService>();
|
||||||
|
logServiceMock = mock<LogService>();
|
||||||
|
offscreenDocumentServiceMock = mock<OffscreenDocumentService>({
|
||||||
|
withDocument: jest.fn((_, __, callback) => callback() as any),
|
||||||
|
});
|
||||||
|
encryptService = new BrowserMultithreadEncryptServiceImplementation(
|
||||||
|
cryptoFunctionServiceMock,
|
||||||
|
logServiceMock,
|
||||||
|
false,
|
||||||
|
offscreenDocumentServiceMock,
|
||||||
|
);
|
||||||
|
manifestVersionSpy.mockReturnValue(3);
|
||||||
|
sendMessageWithResponseSpy.mockResolvedValue(JSON.stringify([]));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decrypts items using web workers if the chrome.offscreen API is not supported", async () => {
|
||||||
|
manifestVersionSpy.mockReturnValue(2);
|
||||||
|
|
||||||
|
await encryptService.decryptItems([], key);
|
||||||
|
|
||||||
|
expect(offscreenDocumentServiceMock.withDocument).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decrypts items using the chrome.offscreen API if it is supported", async () => {
|
||||||
|
sendMessageWithResponseSpy.mockResolvedValue(JSON.stringify(items));
|
||||||
|
|
||||||
|
await encryptService.decryptItems(items, key);
|
||||||
|
|
||||||
|
expect(offscreenDocumentServiceMock.withDocument).toHaveBeenCalledWith(
|
||||||
|
[chrome.offscreen.Reason.WORKERS],
|
||||||
|
"Use web worker to decrypt items.",
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenDecryptItems", {
|
||||||
|
decryptRequest: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty array if the passed items are not defined", async () => {
|
||||||
|
const result = await encryptService.decryptItems(null, key);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty array if the offscreen document message returns an empty value", async () => {
|
||||||
|
sendMessageWithResponseSpy.mockResolvedValue("");
|
||||||
|
|
||||||
|
const result = await encryptService.decryptItems(items, key);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty array if the offscreen document message returns an empty array", async () => {
|
||||||
|
sendMessageWithResponseSpy.mockResolvedValue("[]");
|
||||||
|
|
||||||
|
const result = await encryptService.decryptItems(items, key);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||||
|
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../browser/browser-api";
|
||||||
|
import { OffscreenDocumentService } from "../offscreen-document/abstractions/offscreen-document";
|
||||||
|
|
||||||
|
export class BrowserMultithreadEncryptServiceImplementation extends MultithreadEncryptServiceImplementation {
|
||||||
|
constructor(
|
||||||
|
cryptoFunctionService: CryptoFunctionService,
|
||||||
|
logService: LogService,
|
||||||
|
logMacFailures: boolean,
|
||||||
|
private offscreenDocumentService: OffscreenDocumentService,
|
||||||
|
) {
|
||||||
|
super(cryptoFunctionService, logService, logMacFailures);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles decryption of items, will use the offscreen document if supported.
|
||||||
|
*
|
||||||
|
* @param items - The items to decrypt.
|
||||||
|
* @param key - The key to use for decryption.
|
||||||
|
*/
|
||||||
|
async decryptItems<T extends InitializerMetadata>(
|
||||||
|
items: Decryptable<T>[],
|
||||||
|
key: SymmetricCryptoKey,
|
||||||
|
): Promise<T[]> {
|
||||||
|
if (!this.isOffscreenDocumentSupported()) {
|
||||||
|
return await super.decryptItems(items, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.decryptItemsInOffscreenDocument(items, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts items using the offscreen document api.
|
||||||
|
*
|
||||||
|
* @param items - The items to decrypt.
|
||||||
|
* @param key - The key to use for decryption.
|
||||||
|
*/
|
||||||
|
private async decryptItemsInOffscreenDocument<T extends InitializerMetadata>(
|
||||||
|
items: Decryptable<T>[],
|
||||||
|
key: SymmetricCryptoKey,
|
||||||
|
): Promise<T[]> {
|
||||||
|
if (items == null || items.length < 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
id: Utils.newGuid(),
|
||||||
|
items: items,
|
||||||
|
key: key,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.offscreenDocumentService.withDocument(
|
||||||
|
[chrome.offscreen.Reason.WORKERS],
|
||||||
|
"Use web worker to decrypt items.",
|
||||||
|
async () => {
|
||||||
|
return (await BrowserApi.sendMessageWithResponse("offscreenDecryptItems", {
|
||||||
|
decryptRequest: JSON.stringify(request),
|
||||||
|
})) as string;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseItems = JSON.parse(response);
|
||||||
|
if (responseItems?.length < 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.initializeItems(responseItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the offscreen document api is supported.
|
||||||
|
*/
|
||||||
|
private isOffscreenDocumentSupported() {
|
||||||
|
return (
|
||||||
|
BrowserApi.isManifestVersion(3) &&
|
||||||
|
typeof chrome !== "undefined" &&
|
||||||
|
typeof chrome.offscreen !== "undefined"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
import { BrowserApi } from "../browser/browser-api";
|
import { BrowserApi } from "../browser/browser-api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -20,9 +25,11 @@ describe("ScriptInjectorService", () => {
|
|||||||
let scriptInjectorService: BrowserScriptInjectorService;
|
let scriptInjectorService: BrowserScriptInjectorService;
|
||||||
jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation();
|
jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation();
|
||||||
jest.spyOn(BrowserApi, "isManifestVersion");
|
jest.spyOn(BrowserApi, "isManifestVersion");
|
||||||
|
const platformUtilsService = mock<PlatformUtilsService>();
|
||||||
|
const logService = mock<LogService>();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
scriptInjectorService = new BrowserScriptInjectorService();
|
scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("inject", () => {
|
describe("inject", () => {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
import { BrowserApi } from "../browser/browser-api";
|
import { BrowserApi } from "../browser/browser-api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -7,6 +10,13 @@ import {
|
|||||||
} from "./abstractions/script-injector.service";
|
} from "./abstractions/script-injector.service";
|
||||||
|
|
||||||
export class BrowserScriptInjectorService extends ScriptInjectorService {
|
export class BrowserScriptInjectorService extends ScriptInjectorService {
|
||||||
|
constructor(
|
||||||
|
private readonly platformUtilsService: PlatformUtilsService,
|
||||||
|
private readonly logService: LogService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Facilitates the injection of a script into a tab context. Will adjust
|
* Facilitates the injection of a script into a tab context. Will adjust
|
||||||
* behavior between manifest v2 and v3 based on the passed configuration.
|
* behavior between manifest v2 and v3 based on the passed configuration.
|
||||||
@@ -23,9 +33,26 @@ export class BrowserScriptInjectorService extends ScriptInjectorService {
|
|||||||
const injectionDetails = this.buildInjectionDetails(injectDetails, file);
|
const injectionDetails = this.buildInjectionDetails(injectDetails, file);
|
||||||
|
|
||||||
if (BrowserApi.isManifestVersion(3)) {
|
if (BrowserApi.isManifestVersion(3)) {
|
||||||
await BrowserApi.executeScriptInTab(tabId, injectionDetails, {
|
try {
|
||||||
world: mv3Details?.world ?? "ISOLATED",
|
await BrowserApi.executeScriptInTab(tabId, injectionDetails, {
|
||||||
});
|
world: mv3Details?.world ?? "ISOLATED",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Swallow errors for host permissions, since this is believed to be a Manifest V3 Chrome bug
|
||||||
|
// @TODO remove when the bugged behaviour is resolved
|
||||||
|
if (
|
||||||
|
error.message !==
|
||||||
|
"Cannot access contents of the page. Extension manifest must request permission to access the respective host."
|
||||||
|
) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.platformUtilsService.isDev()) {
|
||||||
|
this.logService.warning(
|
||||||
|
`BrowserApi.executeScriptInTab exception for ${injectDetails.file} in tab ${tabId}: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,15 +62,6 @@ export class DefaultBrowserStateService
|
|||||||
await super.addAccount(account);
|
await super.addAccount(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getIsAuthenticated(options?: StorageOptions): Promise<boolean> {
|
|
||||||
// Firefox Private Mode can clash with non-Private Mode because they both read from the same onDiskOptions
|
|
||||||
// Check that there is an account in memory before considering the user authenticated
|
|
||||||
return (
|
|
||||||
(await super.getIsAuthenticated(options)) &&
|
|
||||||
(await this.getAccount(await this.defaultInMemoryOptions())) != null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overriding the base class to prevent deleting the cache on save. We register a storage listener
|
// Overriding the base class to prevent deleting the cache on save. We register a storage listener
|
||||||
// to delete the cache in the constructor above.
|
// to delete the cache in the constructor above.
|
||||||
protected override async saveAccountToDisk(
|
protected override async saveAccountToDisk(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
|
||||||
|
import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document";
|
||||||
|
|
||||||
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
|
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
|
||||||
|
|
||||||
export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService {
|
export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService {
|
||||||
@@ -8,8 +10,9 @@ export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService
|
|||||||
clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
|
clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
|
||||||
biometricCallback: () => Promise<boolean>,
|
biometricCallback: () => Promise<boolean>,
|
||||||
win: Window & typeof globalThis,
|
win: Window & typeof globalThis,
|
||||||
|
offscreenDocumentService: OffscreenDocumentService,
|
||||||
) {
|
) {
|
||||||
super(clipboardWriteCallback, biometricCallback, win);
|
super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService);
|
||||||
}
|
}
|
||||||
|
|
||||||
override showToast(
|
override showToast(
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { DeviceType } from "@bitwarden/common/enums";
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
|
|
||||||
import { flushPromises } from "../../../autofill/spec/testing-utils";
|
import { flushPromises } from "../../../autofill/spec/testing-utils";
|
||||||
import { SafariApp } from "../../../browser/safariApp";
|
import { SafariApp } from "../../../browser/safariApp";
|
||||||
import { BrowserApi } from "../../browser/browser-api";
|
import { BrowserApi } from "../../browser/browser-api";
|
||||||
|
import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document";
|
||||||
import BrowserClipboardService from "../browser-clipboard.service";
|
import BrowserClipboardService from "../browser-clipboard.service";
|
||||||
|
|
||||||
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
|
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
|
||||||
|
|
||||||
class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService {
|
class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService {
|
||||||
constructor(clipboardSpy: jest.Mock, win: Window & typeof globalThis) {
|
constructor(
|
||||||
super(clipboardSpy, null, win);
|
clipboardSpy: jest.Mock,
|
||||||
|
win: Window & typeof globalThis,
|
||||||
|
offscreenDocumentService: OffscreenDocumentService,
|
||||||
|
) {
|
||||||
|
super(clipboardSpy, null, win, offscreenDocumentService);
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast(
|
showToast(
|
||||||
@@ -24,13 +31,16 @@ class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService {
|
|||||||
|
|
||||||
describe("Browser Utils Service", () => {
|
describe("Browser Utils Service", () => {
|
||||||
let browserPlatformUtilsService: BrowserPlatformUtilsService;
|
let browserPlatformUtilsService: BrowserPlatformUtilsService;
|
||||||
|
let offscreenDocumentService: MockProxy<OffscreenDocumentService>;
|
||||||
const clipboardWriteCallbackSpy = jest.fn();
|
const clipboardWriteCallbackSpy = jest.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
offscreenDocumentService = mock();
|
||||||
(window as any).matchMedia = jest.fn().mockReturnValueOnce({});
|
(window as any).matchMedia = jest.fn().mockReturnValueOnce({});
|
||||||
browserPlatformUtilsService = new TestBrowserPlatformUtilsService(
|
browserPlatformUtilsService = new TestBrowserPlatformUtilsService(
|
||||||
clipboardWriteCallbackSpy,
|
clipboardWriteCallbackSpy,
|
||||||
window,
|
window,
|
||||||
|
offscreenDocumentService,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -223,23 +233,23 @@ describe("Browser Utils Service", () => {
|
|||||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||||
.mockReturnValue(DeviceType.ChromeExtension);
|
.mockReturnValue(DeviceType.ChromeExtension);
|
||||||
getManifestVersionSpy.mockReturnValue(3);
|
getManifestVersionSpy.mockReturnValue(3);
|
||||||
jest.spyOn(BrowserApi, "createOffscreenDocument");
|
|
||||||
jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue(undefined);
|
|
||||||
jest.spyOn(BrowserApi, "closeOffscreenDocument");
|
|
||||||
|
|
||||||
browserPlatformUtilsService.copyToClipboard(text);
|
browserPlatformUtilsService.copyToClipboard(text);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(triggerOffscreenCopyToClipboardSpy).toHaveBeenCalledWith(text);
|
expect(triggerOffscreenCopyToClipboardSpy).toHaveBeenCalledWith(text);
|
||||||
expect(clipboardServiceCopySpy).not.toHaveBeenCalled();
|
expect(clipboardServiceCopySpy).not.toHaveBeenCalled();
|
||||||
expect(BrowserApi.createOffscreenDocument).toHaveBeenCalledWith(
|
expect(offscreenDocumentService.withDocument).toHaveBeenCalledWith(
|
||||||
[chrome.offscreen.Reason.CLIPBOARD],
|
[chrome.offscreen.Reason.CLIPBOARD],
|
||||||
"Write text to the clipboard.",
|
"Write text to the clipboard.",
|
||||||
|
expect.any(Function),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const callback = offscreenDocumentService.withDocument.mock.calls[0][2];
|
||||||
|
await callback();
|
||||||
expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenCopyToClipboard", {
|
expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenCopyToClipboard", {
|
||||||
text,
|
text,
|
||||||
});
|
});
|
||||||
expect(BrowserApi.closeOffscreenDocument).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips the clipboardWriteCallback if the clipboard is clearing", async () => {
|
it("skips the clipboardWriteCallback if the clipboard is clearing", async () => {
|
||||||
@@ -298,18 +308,21 @@ describe("Browser Utils Service", () => {
|
|||||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||||
.mockReturnValue(DeviceType.ChromeExtension);
|
.mockReturnValue(DeviceType.ChromeExtension);
|
||||||
getManifestVersionSpy.mockReturnValue(3);
|
getManifestVersionSpy.mockReturnValue(3);
|
||||||
jest.spyOn(BrowserApi, "createOffscreenDocument");
|
offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) =>
|
||||||
jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue("test");
|
Promise.resolve("test"),
|
||||||
jest.spyOn(BrowserApi, "closeOffscreenDocument");
|
);
|
||||||
|
|
||||||
await browserPlatformUtilsService.readFromClipboard();
|
await browserPlatformUtilsService.readFromClipboard();
|
||||||
|
|
||||||
expect(BrowserApi.createOffscreenDocument).toHaveBeenCalledWith(
|
expect(offscreenDocumentService.withDocument).toHaveBeenCalledWith(
|
||||||
[chrome.offscreen.Reason.CLIPBOARD],
|
[chrome.offscreen.Reason.CLIPBOARD],
|
||||||
"Read text from the clipboard.",
|
"Read text from the clipboard.",
|
||||||
|
expect.any(Function),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const callback = offscreenDocumentService.withDocument.mock.calls[0][2];
|
||||||
|
await callback();
|
||||||
expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenReadFromClipboard");
|
expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenReadFromClipboard");
|
||||||
expect(BrowserApi.closeOffscreenDocument).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an empty string from the offscreen document if the response is not of type string", async () => {
|
it("returns an empty string from the offscreen document if the response is not of type string", async () => {
|
||||||
@@ -317,9 +330,10 @@ describe("Browser Utils Service", () => {
|
|||||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||||
.mockReturnValue(DeviceType.ChromeExtension);
|
.mockReturnValue(DeviceType.ChromeExtension);
|
||||||
getManifestVersionSpy.mockReturnValue(3);
|
getManifestVersionSpy.mockReturnValue(3);
|
||||||
jest.spyOn(BrowserApi, "createOffscreenDocument");
|
|
||||||
jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue(1);
|
jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue(1);
|
||||||
jest.spyOn(BrowserApi, "closeOffscreenDocument");
|
offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) =>
|
||||||
|
Promise.resolve(1),
|
||||||
|
);
|
||||||
|
|
||||||
const result = await browserPlatformUtilsService.readFromClipboard();
|
const result = await browserPlatformUtilsService.readFromClipboard();
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
|
|
||||||
import { SafariApp } from "../../../browser/safariApp";
|
import { SafariApp } from "../../../browser/safariApp";
|
||||||
import { BrowserApi } from "../../browser/browser-api";
|
import { BrowserApi } from "../../browser/browser-api";
|
||||||
|
import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document";
|
||||||
import BrowserClipboardService from "../browser-clipboard.service";
|
import BrowserClipboardService from "../browser-clipboard.service";
|
||||||
|
|
||||||
export abstract class BrowserPlatformUtilsService implements PlatformUtilsService {
|
export abstract class BrowserPlatformUtilsService implements PlatformUtilsService {
|
||||||
@@ -15,6 +16,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
|||||||
private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
|
private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
|
||||||
private biometricCallback: () => Promise<boolean>,
|
private biometricCallback: () => Promise<boolean>,
|
||||||
private globalContext: Window | ServiceWorkerGlobalScope,
|
private globalContext: Window | ServiceWorkerGlobalScope,
|
||||||
|
private offscreenDocumentService: OffscreenDocumentService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static getDevice(globalContext: Window | ServiceWorkerGlobalScope): DeviceType {
|
static getDevice(globalContext: Window | ServiceWorkerGlobalScope): DeviceType {
|
||||||
@@ -316,24 +318,26 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
|||||||
* Triggers the offscreen document API to copy the text to the clipboard.
|
* Triggers the offscreen document API to copy the text to the clipboard.
|
||||||
*/
|
*/
|
||||||
private async triggerOffscreenCopyToClipboard(text: string) {
|
private async triggerOffscreenCopyToClipboard(text: string) {
|
||||||
await BrowserApi.createOffscreenDocument(
|
await this.offscreenDocumentService.withDocument(
|
||||||
[chrome.offscreen.Reason.CLIPBOARD],
|
[chrome.offscreen.Reason.CLIPBOARD],
|
||||||
"Write text to the clipboard.",
|
"Write text to the clipboard.",
|
||||||
|
async () => {
|
||||||
|
await BrowserApi.sendMessageWithResponse("offscreenCopyToClipboard", { text });
|
||||||
|
},
|
||||||
);
|
);
|
||||||
await BrowserApi.sendMessageWithResponse("offscreenCopyToClipboard", { text });
|
|
||||||
BrowserApi.closeOffscreenDocument();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggers the offscreen document API to read the text from the clipboard.
|
* Triggers the offscreen document API to read the text from the clipboard.
|
||||||
*/
|
*/
|
||||||
private async triggerOffscreenReadFromClipboard() {
|
private async triggerOffscreenReadFromClipboard() {
|
||||||
await BrowserApi.createOffscreenDocument(
|
const response = await this.offscreenDocumentService.withDocument(
|
||||||
[chrome.offscreen.Reason.CLIPBOARD],
|
[chrome.offscreen.Reason.CLIPBOARD],
|
||||||
"Read text from the clipboard.",
|
"Read text from the clipboard.",
|
||||||
|
async () => {
|
||||||
|
return await BrowserApi.sendMessageWithResponse("offscreenReadFromClipboard");
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const response = await BrowserApi.sendMessageWithResponse("offscreenReadFromClipboard");
|
|
||||||
BrowserApi.closeOffscreenDocument();
|
|
||||||
if (typeof response === "string") {
|
if (typeof response === "string") {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { ToastService } from "@bitwarden/components";
|
import { ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document";
|
||||||
|
|
||||||
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
|
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
|
||||||
|
|
||||||
export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService {
|
export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService {
|
||||||
@@ -8,8 +10,9 @@ export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService
|
|||||||
clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
|
clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
|
||||||
biometricCallback: () => Promise<boolean>,
|
biometricCallback: () => Promise<boolean>,
|
||||||
win: Window & typeof globalThis,
|
win: Window & typeof globalThis,
|
||||||
|
offscreenDocumentService: OffscreenDocumentService,
|
||||||
) {
|
) {
|
||||||
super(clipboardWriteCallback, biometricCallback, win);
|
super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService);
|
||||||
}
|
}
|
||||||
|
|
||||||
override showToast(
|
override showToast(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.comp
|
|||||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||||
|
import { PremiumComponent } from "../billing/popup/settings/premium.component";
|
||||||
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
|
||||||
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
||||||
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
|
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
|
||||||
@@ -51,7 +52,6 @@ import { ExcludedDomainsComponent } from "./settings/excluded-domains.component"
|
|||||||
import { FoldersComponent } from "./settings/folders.component";
|
import { FoldersComponent } from "./settings/folders.component";
|
||||||
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
||||||
import { OptionsComponent } from "./settings/options.component";
|
import { OptionsComponent } from "./settings/options.component";
|
||||||
import { PremiumComponent } from "./settings/premium.component";
|
|
||||||
import { SettingsComponent } from "./settings/settings.component";
|
import { SettingsComponent } from "./settings/settings.component";
|
||||||
import { SyncComponent } from "./settings/sync.component";
|
import { SyncComponent } from "./settings/sync.component";
|
||||||
import { TabsComponent } from "./tabs.component";
|
import { TabsComponent } from "./tabs.component";
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.comp
|
|||||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||||
|
import { PremiumComponent } from "../billing/popup/settings/premium.component";
|
||||||
import { HeaderComponent } from "../platform/popup/header.component";
|
import { HeaderComponent } from "../platform/popup/header.component";
|
||||||
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
|
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
|
||||||
import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component";
|
import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component";
|
||||||
@@ -70,14 +71,12 @@ import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.
|
|||||||
import { AppRoutingModule } from "./app-routing.module";
|
import { AppRoutingModule } from "./app-routing.module";
|
||||||
import { AppComponent } from "./app.component";
|
import { AppComponent } from "./app.component";
|
||||||
import { PopOutComponent } from "./components/pop-out.component";
|
import { PopOutComponent } from "./components/pop-out.component";
|
||||||
import { PrivateModeWarningComponent } from "./components/private-mode-warning.component";
|
|
||||||
import { UserVerificationComponent } from "./components/user-verification.component";
|
import { UserVerificationComponent } from "./components/user-verification.component";
|
||||||
import { ServicesModule } from "./services/services.module";
|
import { ServicesModule } from "./services/services.module";
|
||||||
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
|
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
|
||||||
import { FoldersComponent } from "./settings/folders.component";
|
import { FoldersComponent } from "./settings/folders.component";
|
||||||
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
||||||
import { OptionsComponent } from "./settings/options.component";
|
import { OptionsComponent } from "./settings/options.component";
|
||||||
import { PremiumComponent } from "./settings/premium.component";
|
|
||||||
import { SettingsComponent } from "./settings/settings.component";
|
import { SettingsComponent } from "./settings/settings.component";
|
||||||
import { SyncComponent } from "./settings/sync.component";
|
import { SyncComponent } from "./settings/sync.component";
|
||||||
import { VaultTimeoutInputComponent } from "./settings/vault-timeout-input.component";
|
import { VaultTimeoutInputComponent } from "./settings/vault-timeout-input.component";
|
||||||
@@ -150,7 +149,6 @@ import "../platform/popup/locales";
|
|||||||
PasswordHistoryComponent,
|
PasswordHistoryComponent,
|
||||||
PopOutComponent,
|
PopOutComponent,
|
||||||
PremiumComponent,
|
PremiumComponent,
|
||||||
PrivateModeWarningComponent,
|
|
||||||
RegisterComponent,
|
RegisterComponent,
|
||||||
SendAddEditComponent,
|
SendAddEditComponent,
|
||||||
SendGroupingsComponent,
|
SendGroupingsComponent,
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<app-callout class="app-private-mode-warning" type="warning" *ngIf="showWarning">
|
|
||||||
{{ "privateModeWarning" | i18n }}
|
|
||||||
<a href="https://bitwarden.com/help/article/private-mode/" target="_blank" rel="noreferrer">{{
|
|
||||||
"learnMore" | i18n
|
|
||||||
}}</a>
|
|
||||||
</app-callout>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
|
||||||
|
|
||||||
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-private-mode-warning",
|
|
||||||
templateUrl: "private-mode-warning.component.html",
|
|
||||||
})
|
|
||||||
export class PrivateModeWarningComponent implements OnInit {
|
|
||||||
showWarning = false;
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.showWarning = BrowserPopupUtils.inPrivateMode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -111,11 +111,6 @@ app-home {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-private-mode-warning {
|
|
||||||
display: block;
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.body-sm,
|
body.body-sm,
|
||||||
body.body-xs {
|
body.body-xs {
|
||||||
app-home {
|
app-home {
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
|
||||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
|
||||||
|
|
||||||
export class PopupSearchService extends SearchService {
|
|
||||||
constructor(logService: LogService, i18nService: I18nService, stateProvider: StateProvider) {
|
|
||||||
super(logService, i18nService, stateProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearIndex(): Promise<void> {
|
|
||||||
throw new Error("Not available.");
|
|
||||||
}
|
|
||||||
|
|
||||||
indexCiphers(): Promise<void> {
|
|
||||||
throw new Error("Not available.");
|
|
||||||
}
|
|
||||||
|
|
||||||
async getIndexForSearch() {
|
|
||||||
return await super.getIndexForSearch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,6 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.
|
|||||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||||
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
|
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
@@ -28,7 +27,9 @@ import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/a
|
|||||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||||
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||||
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
@@ -53,6 +54,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.
|
|||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
@@ -61,6 +63,7 @@ import {
|
|||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
||||||
@@ -96,10 +99,13 @@ import { runInsideAngular } from "../../platform/browser/run-inside-angular.oper
|
|||||||
/* eslint-disable no-restricted-imports */
|
/* eslint-disable no-restricted-imports */
|
||||||
import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sender";
|
import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sender";
|
||||||
/* eslint-enable no-restricted-imports */
|
/* eslint-enable no-restricted-imports */
|
||||||
|
import { OffscreenDocumentService } from "../../platform/offscreen-document/abstractions/offscreen-document";
|
||||||
|
import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service";
|
||||||
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
||||||
import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service";
|
import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service";
|
||||||
import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service";
|
import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service";
|
||||||
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
|
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
|
||||||
|
import { BrowserCryptoService } from "../../platform/services/browser-crypto.service";
|
||||||
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
|
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
|
||||||
import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
|
import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
|
||||||
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
|
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
|
||||||
@@ -118,20 +124,18 @@ import { VaultFilterService } from "../../vault/services/vault-filter.service";
|
|||||||
import { DebounceNavigationService } from "./debounce-navigation.service";
|
import { DebounceNavigationService } from "./debounce-navigation.service";
|
||||||
import { InitService } from "./init.service";
|
import { InitService } from "./init.service";
|
||||||
import { PopupCloseWarningService } from "./popup-close-warning.service";
|
import { PopupCloseWarningService } from "./popup-close-warning.service";
|
||||||
import { PopupSearchService } from "./popup-search.service";
|
|
||||||
|
|
||||||
const OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE = new SafeInjectionToken<
|
const OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE = new SafeInjectionToken<
|
||||||
AbstractStorageService & ObservableStorageService
|
AbstractStorageService & ObservableStorageService
|
||||||
>("OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE");
|
>("OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE");
|
||||||
|
|
||||||
const needsBackgroundInit = BrowserPopupUtils.backgroundInitializationRequired();
|
const needsBackgroundInit = BrowserPopupUtils.backgroundInitializationRequired();
|
||||||
const isPrivateMode = BrowserPopupUtils.inPrivateMode();
|
|
||||||
const mainBackground: MainBackground = needsBackgroundInit
|
const mainBackground: MainBackground = needsBackgroundInit
|
||||||
? createLocalBgService()
|
? createLocalBgService()
|
||||||
: BrowserApi.getBackgroundPage().bitwardenMain;
|
: BrowserApi.getBackgroundPage().bitwardenMain;
|
||||||
|
|
||||||
function createLocalBgService() {
|
function createLocalBgService() {
|
||||||
const localBgService = new MainBackground(isPrivateMode, true);
|
const localBgService = new MainBackground(true);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
localBgService.bootstrap();
|
localBgService.bootstrap();
|
||||||
@@ -176,26 +180,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useFactory: getBgService<SsoLoginServiceAbstraction>("ssoLoginService"),
|
useFactory: getBgService<SsoLoginServiceAbstraction>("ssoLoginService"),
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: SearchServiceAbstraction,
|
|
||||||
useClass: PopupSearchService,
|
|
||||||
deps: [LogService, I18nServiceAbstraction, StateProvider],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
|
||||||
provide: CipherService,
|
|
||||||
useFactory: getBgService<CipherService>("cipherService"),
|
|
||||||
deps: [],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: CryptoFunctionService,
|
provide: CryptoFunctionService,
|
||||||
useFactory: () => new WebCryptoFunctionService(window),
|
useFactory: () => new WebCryptoFunctionService(window),
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: CollectionService,
|
|
||||||
useFactory: getBgService<CollectionService>("collectionService"),
|
|
||||||
deps: [],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: LogService,
|
provide: LogService,
|
||||||
useFactory: (platformUtilsService: PlatformUtilsService) =>
|
useFactory: (platformUtilsService: PlatformUtilsService) =>
|
||||||
@@ -220,12 +209,48 @@ const safeProviders: SafeProvider[] = [
|
|||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: CryptoService,
|
provide: CryptoService,
|
||||||
useFactory: (encryptService: EncryptService) => {
|
useFactory: (
|
||||||
const cryptoService = getBgService<CryptoService>("cryptoService")();
|
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
|
keyGenerationService: KeyGenerationService,
|
||||||
|
cryptoFunctionService: CryptoFunctionService,
|
||||||
|
encryptService: EncryptService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
logService: LogService,
|
||||||
|
stateService: StateServiceAbstraction,
|
||||||
|
accountService: AccountServiceAbstraction,
|
||||||
|
stateProvider: StateProvider,
|
||||||
|
biometricStateService: BiometricStateService,
|
||||||
|
kdfConfigService: KdfConfigService,
|
||||||
|
) => {
|
||||||
|
const cryptoService = new BrowserCryptoService(
|
||||||
|
masterPasswordService,
|
||||||
|
keyGenerationService,
|
||||||
|
cryptoFunctionService,
|
||||||
|
encryptService,
|
||||||
|
platformUtilsService,
|
||||||
|
logService,
|
||||||
|
stateService,
|
||||||
|
accountService,
|
||||||
|
stateProvider,
|
||||||
|
biometricStateService,
|
||||||
|
kdfConfigService,
|
||||||
|
);
|
||||||
new ContainerService(cryptoService, encryptService).attachToGlobal(self);
|
new ContainerService(cryptoService, encryptService).attachToGlobal(self);
|
||||||
return cryptoService;
|
return cryptoService;
|
||||||
},
|
},
|
||||||
deps: [EncryptService],
|
deps: [
|
||||||
|
InternalMasterPasswordServiceAbstraction,
|
||||||
|
KeyGenerationService,
|
||||||
|
CryptoFunctionService,
|
||||||
|
EncryptService,
|
||||||
|
PlatformUtilsService,
|
||||||
|
LogService,
|
||||||
|
StateServiceAbstraction,
|
||||||
|
AccountServiceAbstraction,
|
||||||
|
StateProvider,
|
||||||
|
BiometricStateService,
|
||||||
|
KdfConfigService,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: TotpServiceAbstraction,
|
provide: TotpServiceAbstraction,
|
||||||
@@ -247,9 +272,17 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useFactory: getBgService<DevicesServiceAbstraction>("devicesService"),
|
useFactory: getBgService<DevicesServiceAbstraction>("devicesService"),
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: OffscreenDocumentService,
|
||||||
|
useClass: DefaultOffscreenDocumentService,
|
||||||
|
deps: [],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: PlatformUtilsService,
|
provide: PlatformUtilsService,
|
||||||
useFactory: (toastService: ToastService) => {
|
useFactory: (
|
||||||
|
toastService: ToastService,
|
||||||
|
offscreenDocumentService: OffscreenDocumentService,
|
||||||
|
) => {
|
||||||
return new ForegroundPlatformUtilsService(
|
return new ForegroundPlatformUtilsService(
|
||||||
toastService,
|
toastService,
|
||||||
(clipboardValue: string, clearMs: number) => {
|
(clipboardValue: string, clearMs: number) => {
|
||||||
@@ -266,9 +299,10 @@ const safeProviders: SafeProvider[] = [
|
|||||||
return response.result;
|
return response.result;
|
||||||
},
|
},
|
||||||
window,
|
window,
|
||||||
|
offscreenDocumentService,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
deps: [ToastService],
|
deps: [ToastService, OffscreenDocumentService],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: PasswordGenerationServiceAbstraction,
|
provide: PasswordGenerationServiceAbstraction,
|
||||||
@@ -312,7 +346,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: ScriptInjectorService,
|
provide: ScriptInjectorService,
|
||||||
useClass: BrowserScriptInjectorService,
|
useClass: BrowserScriptInjectorService,
|
||||||
deps: [],
|
deps: [PlatformUtilsService, LogService],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: KeyConnectorService,
|
provide: KeyConnectorService,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { PolicyService } from "@bitwarden/common/admin-console/services/policy/p
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { Importer, ImportResult, ImportServiceAbstraction } from "@bitwarden/importer/core";
|
import { Importer, ImportResult, ImportServiceAbstraction } from "@bitwarden/importer/core";
|
||||||
|
|
||||||
@@ -38,10 +40,12 @@ describe("FilelessImporterBackground ", () => {
|
|||||||
const notificationBackground = mock<NotificationBackground>();
|
const notificationBackground = mock<NotificationBackground>();
|
||||||
const importService = mock<ImportServiceAbstraction>();
|
const importService = mock<ImportServiceAbstraction>();
|
||||||
const syncService = mock<SyncService>();
|
const syncService = mock<SyncService>();
|
||||||
|
const platformUtilsService = mock<PlatformUtilsService>();
|
||||||
|
const logService = mock<LogService>();
|
||||||
let scriptInjectorService: BrowserScriptInjectorService;
|
let scriptInjectorService: BrowserScriptInjectorService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
scriptInjectorService = new BrowserScriptInjectorService();
|
scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService);
|
||||||
filelessImporterBackground = new FilelessImporterBackground(
|
filelessImporterBackground = new FilelessImporterBackground(
|
||||||
configService,
|
configService,
|
||||||
authService,
|
authService,
|
||||||
|
|||||||
@@ -70,13 +70,13 @@ export class Fido2Background implements Fido2BackgroundInterface {
|
|||||||
*/
|
*/
|
||||||
async injectFido2ContentScriptsInAllTabs() {
|
async injectFido2ContentScriptsInAllTabs() {
|
||||||
const tabs = await BrowserApi.tabsQuery({});
|
const tabs = await BrowserApi.tabsQuery({});
|
||||||
|
|
||||||
for (let index = 0; index < tabs.length; index++) {
|
for (let index = 0; index < tabs.length; index++) {
|
||||||
const tab = tabs[index];
|
const tab = tabs[index];
|
||||||
if (!tab.url?.startsWith("https")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
void this.injectFido2ContentScripts(tab);
|
if (tab.url?.startsWith("https")) {
|
||||||
|
void this.injectFido2ContentScripts(tab);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,17 +15,43 @@ jest.mock("../../../autofill/utils", () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const originalGlobalThis = globalThis;
|
||||||
|
const mockGlobalThisDocument = {
|
||||||
|
...originalGlobalThis.document,
|
||||||
|
contentType: "text/html",
|
||||||
|
location: {
|
||||||
|
...originalGlobalThis.document.location,
|
||||||
|
href: "https://localhost",
|
||||||
|
origin: "https://localhost",
|
||||||
|
protocol: "https:",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe("Fido2 Content Script", () => {
|
describe("Fido2 Content Script", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
(jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(
|
||||||
|
() => mockGlobalThisDocument,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
let messenger: Messenger;
|
let messenger: Messenger;
|
||||||
const messengerForDOMCommunicationSpy = jest
|
const messengerForDOMCommunicationSpy = jest
|
||||||
.spyOn(Messenger, "forDOMCommunication")
|
.spyOn(Messenger, "forDOMCommunication")
|
||||||
.mockImplementation((window) => {
|
.mockImplementation((context) => {
|
||||||
const windowOrigin = window.location.origin;
|
const windowOrigin = context.location.origin;
|
||||||
|
|
||||||
messenger = new Messenger({
|
messenger = new Messenger({
|
||||||
postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]),
|
postMessage: (message, port) => context.postMessage(message, windowOrigin, [port]),
|
||||||
addEventListener: (listener) => window.addEventListener("message", listener),
|
addEventListener: (listener) => context.addEventListener("message", listener),
|
||||||
removeEventListener: (listener) => window.removeEventListener("message", listener),
|
removeEventListener: (listener) => context.removeEventListener("message", listener),
|
||||||
});
|
});
|
||||||
messenger.destroy = jest.fn();
|
messenger.destroy = jest.fn();
|
||||||
return messenger;
|
return messenger;
|
||||||
@@ -33,16 +59,6 @@ describe("Fido2 Content Script", () => {
|
|||||||
const portSpy: MockProxy<chrome.runtime.Port> = createPortSpyMock(Fido2PortName.InjectedScript);
|
const portSpy: MockProxy<chrome.runtime.Port> = createPortSpyMock(Fido2PortName.InjectedScript);
|
||||||
chrome.runtime.connect = jest.fn(() => portSpy);
|
chrome.runtime.connect = jest.fn(() => portSpy);
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
Object.defineProperty(document, "contentType", {
|
|
||||||
value: "text/html",
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.clearAllMocks();
|
|
||||||
jest.resetModules();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("destroys the messenger when the port is disconnected", () => {
|
it("destroys the messenger when the port is disconnected", () => {
|
||||||
require("./content-script");
|
require("./content-script");
|
||||||
|
|
||||||
@@ -151,11 +167,31 @@ describe("Fido2 Content Script", () => {
|
|||||||
await expect(result).rejects.toEqual(errorMessage);
|
await expect(result).rejects.toEqual(errorMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips initializing the content script if the document content type is not 'text/html'", () => {
|
it("skips initializing if the document content type is not 'text/html'", () => {
|
||||||
Object.defineProperty(document, "contentType", {
|
jest.clearAllMocks();
|
||||||
value: "application/json",
|
|
||||||
writable: true,
|
(jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({
|
||||||
});
|
...mockGlobalThisDocument,
|
||||||
|
contentType: "application/json",
|
||||||
|
}));
|
||||||
|
|
||||||
|
require("./content-script");
|
||||||
|
|
||||||
|
expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips initializing if the document location protocol is not 'https'", () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
(jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({
|
||||||
|
...mockGlobalThisDocument,
|
||||||
|
location: {
|
||||||
|
...mockGlobalThisDocument.location,
|
||||||
|
href: "http://localhost",
|
||||||
|
origin: "http://localhost",
|
||||||
|
protocol: "http:",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
require("./content-script");
|
require("./content-script");
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ import {
|
|||||||
import { MessageWithMetadata, Messenger } from "./messaging/messenger";
|
import { MessageWithMetadata, Messenger } from "./messaging/messenger";
|
||||||
|
|
||||||
(function (globalContext) {
|
(function (globalContext) {
|
||||||
if (globalContext.document.contentType !== "text/html") {
|
const shouldExecuteContentScript =
|
||||||
|
globalContext.document.contentType === "text/html" &&
|
||||||
|
globalContext.document.location.protocol === "https:";
|
||||||
|
|
||||||
|
if (!shouldExecuteContentScript) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ import { MessageType } from "./messaging/message";
|
|||||||
import { Messenger } from "./messaging/messenger";
|
import { Messenger } from "./messaging/messenger";
|
||||||
|
|
||||||
(function (globalContext) {
|
(function (globalContext) {
|
||||||
if (globalContext.document.contentType !== "text/html") {
|
const shouldExecuteContentScript =
|
||||||
|
globalContext.document.contentType === "text/html" &&
|
||||||
|
globalContext.document.location.protocol === "https:";
|
||||||
|
|
||||||
|
if (!shouldExecuteContentScript) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BrowserPublicKeyCredential = globalContext.PublicKeyCredential;
|
const BrowserPublicKeyCredential = globalContext.PublicKeyCredential;
|
||||||
const BrowserNavigatorCredentials = navigator.credentials;
|
const BrowserNavigatorCredentials = navigator.credentials;
|
||||||
const BrowserAuthenticatorAttestationResponse = globalContext.AuthenticatorAttestationResponse;
|
const BrowserAuthenticatorAttestationResponse = globalContext.AuthenticatorAttestationResponse;
|
||||||
|
|||||||
@@ -10,17 +10,29 @@ import { WebauthnUtils } from "../webauthn-utils";
|
|||||||
import { MessageType } from "./messaging/message";
|
import { MessageType } from "./messaging/message";
|
||||||
import { Messenger } from "./messaging/messenger";
|
import { Messenger } from "./messaging/messenger";
|
||||||
|
|
||||||
|
const originalGlobalThis = globalThis;
|
||||||
|
const mockGlobalThisDocument = {
|
||||||
|
...originalGlobalThis.document,
|
||||||
|
contentType: "text/html",
|
||||||
|
location: {
|
||||||
|
...originalGlobalThis.document.location,
|
||||||
|
href: "https://localhost",
|
||||||
|
origin: "https://localhost",
|
||||||
|
protocol: "https:",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let messenger: Messenger;
|
let messenger: Messenger;
|
||||||
jest.mock("./messaging/messenger", () => {
|
jest.mock("./messaging/messenger", () => {
|
||||||
return {
|
return {
|
||||||
Messenger: class extends jest.requireActual("./messaging/messenger").Messenger {
|
Messenger: class extends jest.requireActual("./messaging/messenger").Messenger {
|
||||||
static forDOMCommunication: any = jest.fn((window) => {
|
static forDOMCommunication: any = jest.fn((context) => {
|
||||||
const windowOrigin = window.location.origin;
|
const windowOrigin = context.location.origin;
|
||||||
|
|
||||||
messenger = new Messenger({
|
messenger = new Messenger({
|
||||||
postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]),
|
postMessage: (message, port) => context.postMessage(message, windowOrigin, [port]),
|
||||||
addEventListener: (listener) => window.addEventListener("message", listener),
|
addEventListener: (listener) => context.addEventListener("message", listener),
|
||||||
removeEventListener: (listener) => window.removeEventListener("message", listener),
|
removeEventListener: (listener) => context.removeEventListener("message", listener),
|
||||||
});
|
});
|
||||||
messenger.destroy = jest.fn();
|
messenger.destroy = jest.fn();
|
||||||
return messenger;
|
return messenger;
|
||||||
@@ -31,6 +43,10 @@ jest.mock("./messaging/messenger", () => {
|
|||||||
jest.mock("../webauthn-utils");
|
jest.mock("../webauthn-utils");
|
||||||
|
|
||||||
describe("Fido2 page script with native WebAuthn support", () => {
|
describe("Fido2 page script with native WebAuthn support", () => {
|
||||||
|
(jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(
|
||||||
|
() => mockGlobalThisDocument,
|
||||||
|
);
|
||||||
|
|
||||||
const mockCredentialCreationOptions = createCredentialCreationOptionsMock();
|
const mockCredentialCreationOptions = createCredentialCreationOptionsMock();
|
||||||
const mockCreateCredentialsResult = createCreateCredentialResultMock();
|
const mockCreateCredentialsResult = createCreateCredentialResultMock();
|
||||||
const mockCredentialRequestOptions = createCredentialRequestOptionsMock();
|
const mockCredentialRequestOptions = createCredentialRequestOptionsMock();
|
||||||
@@ -39,9 +55,12 @@ describe("Fido2 page script with native WebAuthn support", () => {
|
|||||||
|
|
||||||
require("./page-script");
|
require("./page-script");
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
jest.resetModules();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("creating WebAuthn credentials", () => {
|
describe("creating WebAuthn credentials", () => {
|
||||||
@@ -118,4 +137,42 @@ describe("Fido2 page script with native WebAuthn support", () => {
|
|||||||
expect(messenger.destroy).toHaveBeenCalled();
|
expect(messenger.destroy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("content script execution", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
jest.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips initializing if the document content type is not 'text/html'", () => {
|
||||||
|
jest.spyOn(Messenger, "forDOMCommunication");
|
||||||
|
|
||||||
|
(jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({
|
||||||
|
...mockGlobalThisDocument,
|
||||||
|
contentType: "json/application",
|
||||||
|
}));
|
||||||
|
|
||||||
|
require("./content-script");
|
||||||
|
|
||||||
|
expect(Messenger.forDOMCommunication).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips initializing if the document location protocol is not 'https'", () => {
|
||||||
|
jest.spyOn(Messenger, "forDOMCommunication");
|
||||||
|
|
||||||
|
(jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({
|
||||||
|
...mockGlobalThisDocument,
|
||||||
|
location: {
|
||||||
|
...mockGlobalThisDocument.location,
|
||||||
|
href: "http://localhost",
|
||||||
|
origin: "http://localhost",
|
||||||
|
protocol: "http:",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
require("./content-script");
|
||||||
|
|
||||||
|
expect(Messenger.forDOMCommunication).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,17 +9,29 @@ import { WebauthnUtils } from "../webauthn-utils";
|
|||||||
import { MessageType } from "./messaging/message";
|
import { MessageType } from "./messaging/message";
|
||||||
import { Messenger } from "./messaging/messenger";
|
import { Messenger } from "./messaging/messenger";
|
||||||
|
|
||||||
|
const originalGlobalThis = globalThis;
|
||||||
|
const mockGlobalThisDocument = {
|
||||||
|
...originalGlobalThis.document,
|
||||||
|
contentType: "text/html",
|
||||||
|
location: {
|
||||||
|
...originalGlobalThis.document.location,
|
||||||
|
href: "https://localhost",
|
||||||
|
origin: "https://localhost",
|
||||||
|
protocol: "https:",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let messenger: Messenger;
|
let messenger: Messenger;
|
||||||
jest.mock("./messaging/messenger", () => {
|
jest.mock("./messaging/messenger", () => {
|
||||||
return {
|
return {
|
||||||
Messenger: class extends jest.requireActual("./messaging/messenger").Messenger {
|
Messenger: class extends jest.requireActual("./messaging/messenger").Messenger {
|
||||||
static forDOMCommunication: any = jest.fn((window) => {
|
static forDOMCommunication: any = jest.fn((context) => {
|
||||||
const windowOrigin = window.location.origin;
|
const windowOrigin = context.location.origin;
|
||||||
|
|
||||||
messenger = new Messenger({
|
messenger = new Messenger({
|
||||||
postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]),
|
postMessage: (message, port) => context.postMessage(message, windowOrigin, [port]),
|
||||||
addEventListener: (listener) => window.addEventListener("message", listener),
|
addEventListener: (listener) => context.addEventListener("message", listener),
|
||||||
removeEventListener: (listener) => window.removeEventListener("message", listener),
|
removeEventListener: (listener) => context.removeEventListener("message", listener),
|
||||||
});
|
});
|
||||||
messenger.destroy = jest.fn();
|
messenger.destroy = jest.fn();
|
||||||
return messenger;
|
return messenger;
|
||||||
@@ -30,15 +42,22 @@ jest.mock("./messaging/messenger", () => {
|
|||||||
jest.mock("../webauthn-utils");
|
jest.mock("../webauthn-utils");
|
||||||
|
|
||||||
describe("Fido2 page script without native WebAuthn support", () => {
|
describe("Fido2 page script without native WebAuthn support", () => {
|
||||||
|
(jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(
|
||||||
|
() => mockGlobalThisDocument,
|
||||||
|
);
|
||||||
|
|
||||||
const mockCredentialCreationOptions = createCredentialCreationOptionsMock();
|
const mockCredentialCreationOptions = createCredentialCreationOptionsMock();
|
||||||
const mockCreateCredentialsResult = createCreateCredentialResultMock();
|
const mockCreateCredentialsResult = createCreateCredentialResultMock();
|
||||||
const mockCredentialRequestOptions = createCredentialRequestOptionsMock();
|
const mockCredentialRequestOptions = createCredentialRequestOptionsMock();
|
||||||
const mockCredentialAssertResult = createAssertCredentialResultMock();
|
const mockCredentialAssertResult = createAssertCredentialResultMock();
|
||||||
require("./page-script");
|
require("./page-script");
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
jest.resetModules();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("creating WebAuthn credentials", () => {
|
describe("creating WebAuthn credentials", () => {
|
||||||
|
|||||||
@@ -292,8 +292,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
|||||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(
|
const ciphers = await this.cipherService.getAllDecryptedForUrl(
|
||||||
this.url,
|
this.url,
|
||||||
otherTypes.length > 0 ? otherTypes : null,
|
otherTypes.length > 0 ? otherTypes : null,
|
||||||
null,
|
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.loginCiphers = [];
|
this.loginCiphers = [];
|
||||||
|
|||||||
@@ -611,7 +611,6 @@ export class Main {
|
|||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.folderService,
|
this.folderService,
|
||||||
this.collectionService,
|
this.collectionService,
|
||||||
this.cryptoService,
|
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
this.messagingService,
|
this.messagingService,
|
||||||
this.searchService,
|
this.searchService,
|
||||||
|
|||||||
@@ -146,6 +146,8 @@ export class NativeMessagingMain {
|
|||||||
allowed_origins: [
|
allowed_origins: [
|
||||||
// Chrome extension
|
// Chrome extension
|
||||||
"chrome-extension://nngceckbapebfimnlniiiahkandclblb/",
|
"chrome-extension://nngceckbapebfimnlniiiahkandclblb/",
|
||||||
|
// Chrome beta extension
|
||||||
|
"chrome-extension://hccnnhgbibccigepcmlgppchkpfdophk/",
|
||||||
// Edge extension
|
// Edge extension
|
||||||
"chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/",
|
"chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/",
|
||||||
// Opera extension
|
// Opera extension
|
||||||
|
|||||||
@@ -50,7 +50,12 @@
|
|||||||
</bit-tab>
|
</bit-tab>
|
||||||
|
|
||||||
<bit-tab label="{{ 'collections' | i18n }}">
|
<bit-tab label="{{ 'collections' | i18n }}">
|
||||||
<p>{{ "editGroupCollectionsDesc" | i18n }}</p>
|
<p>
|
||||||
|
{{ "editGroupCollectionsDesc" | i18n }}
|
||||||
|
<span *ngIf="!(allowAdminAccessToAllCollectionItems$ | async)">
|
||||||
|
{{ "editGroupCollectionsRestrictionsDesc" | i18n }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
<div *ngIf="!(flexibleCollectionsEnabled$ | async)" class="tw-my-3">
|
<div *ngIf="!(flexibleCollectionsEnabled$ | async)" class="tw-my-3">
|
||||||
<input type="checkbox" formControlName="accessAll" id="accessAll" />
|
<input type="checkbox" formControlName="accessAll" id="accessAll" />
|
||||||
<label class="tw-mb-0 tw-text-lg" for="accessAll">{{
|
<label class="tw-mb-0 tw-text-lg" for="accessAll">{{
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import {
|
|||||||
of,
|
of,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
Subject,
|
Subject,
|
||||||
switchMap,
|
|
||||||
takeUntil,
|
takeUntil,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
@@ -26,12 +26,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
|
||||||
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
|
|
||||||
import { Collection } from "@bitwarden/common/vault/models/domain/collection";
|
|
||||||
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { CollectionAdminService } from "../../../vault/core/collection-admin.service";
|
||||||
|
import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view";
|
||||||
import { InternalGroupService as GroupService, GroupView } from "../core";
|
import { InternalGroupService as GroupService, GroupView } from "../core";
|
||||||
import {
|
import {
|
||||||
AccessItemType,
|
AccessItemType,
|
||||||
@@ -95,9 +93,15 @@ export const openGroupAddEditDialog = (
|
|||||||
templateUrl: "group-add-edit.component.html",
|
templateUrl: "group-add-edit.component.html",
|
||||||
})
|
})
|
||||||
export class GroupAddEditComponent implements OnInit, OnDestroy {
|
export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||||
protected flexibleCollectionsEnabled$ = this.organizationService
|
private organization$ = this.organizationService
|
||||||
.get$(this.organizationId)
|
.get$(this.organizationId)
|
||||||
.pipe(map((o) => o?.flexibleCollections));
|
.pipe(shareReplay({ refCount: true }));
|
||||||
|
protected flexibleCollectionsEnabled$ = this.organization$.pipe(
|
||||||
|
map((o) => o?.flexibleCollections),
|
||||||
|
);
|
||||||
|
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.FlexibleCollectionsV1,
|
||||||
|
);
|
||||||
|
|
||||||
protected PermissionMode = PermissionMode;
|
protected PermissionMode = PermissionMode;
|
||||||
protected ResultType = GroupAddEditDialogResultType;
|
protected ResultType = GroupAddEditDialogResultType;
|
||||||
@@ -131,27 +135,9 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
private get orgCollections$() {
|
private orgCollections$ = from(this.collectionAdminService.getAll(this.organizationId)).pipe(
|
||||||
return from(this.apiService.getCollections(this.organizationId)).pipe(
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
switchMap((response) => {
|
);
|
||||||
return from(
|
|
||||||
this.collectionService.decryptMany(
|
|
||||||
response.data.map(
|
|
||||||
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
map((collections) =>
|
|
||||||
collections.map<AccessItemView>((c) => ({
|
|
||||||
id: c.id,
|
|
||||||
type: AccessItemType.Collection,
|
|
||||||
labelName: c.name,
|
|
||||||
listName: c.name,
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> {
|
private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> {
|
||||||
return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe(
|
return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe(
|
||||||
@@ -197,23 +183,24 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
);
|
);
|
||||||
|
|
||||||
restrictGroupAccess$ = combineLatest([
|
allowAdminAccessToAllCollectionItems$ = combineLatest([
|
||||||
this.organizationService.get$(this.organizationId),
|
this.organization$,
|
||||||
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
|
this.flexibleCollectionsV1Enabled$,
|
||||||
this.groupDetails$,
|
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(
|
map(([organization, flexibleCollectionsV1Enabled]) => {
|
||||||
([organization, flexibleCollectionsV1Enabled, group]) =>
|
if (!flexibleCollectionsV1Enabled || !organization.flexibleCollections) {
|
||||||
// Feature flag conditionals
|
return true;
|
||||||
flexibleCollectionsV1Enabled &&
|
}
|
||||||
organization.flexibleCollections &&
|
|
||||||
// Business logic conditionals
|
return organization.allowAdminAccessToAllCollectionItems;
|
||||||
!organization.allowAdminAccessToAllCollectionItems &&
|
}),
|
||||||
group !== undefined,
|
|
||||||
),
|
|
||||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
restrictGroupAccess$ = combineLatest([
|
||||||
|
this.allowAdminAccessToAllCollectionItems$,
|
||||||
|
this.groupDetails$,
|
||||||
|
]).pipe(map(([allowAdminAccess, groupDetails]) => !allowAdminAccess && groupDetails != null));
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
|
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
|
||||||
private dialogRef: DialogRef<GroupAddEditDialogResultType>,
|
private dialogRef: DialogRef<GroupAddEditDialogResultType>,
|
||||||
@@ -221,7 +208,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||||||
private organizationUserService: OrganizationUserService,
|
private organizationUserService: OrganizationUserService,
|
||||||
private groupService: GroupService,
|
private groupService: GroupService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private collectionService: CollectionService,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
@@ -230,6 +216,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
|
private collectionAdminService: CollectionAdminService,
|
||||||
) {
|
) {
|
||||||
this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info;
|
this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info;
|
||||||
}
|
}
|
||||||
@@ -244,48 +231,61 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||||||
this.groupDetails$,
|
this.groupDetails$,
|
||||||
this.restrictGroupAccess$,
|
this.restrictGroupAccess$,
|
||||||
this.accountService.activeAccount$,
|
this.accountService.activeAccount$,
|
||||||
|
this.organization$,
|
||||||
|
this.flexibleCollectionsV1Enabled$,
|
||||||
])
|
])
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe(([collections, members, group, restrictGroupAccess, activeAccount]) => {
|
.subscribe(
|
||||||
this.collections = collections;
|
([
|
||||||
this.members = members;
|
collections,
|
||||||
this.group = group;
|
members,
|
||||||
|
group,
|
||||||
if (this.group != undefined) {
|
restrictGroupAccess,
|
||||||
// Must detect changes so that AccessSelector @Inputs() are aware of the latest
|
activeAccount,
|
||||||
// collections/members set above, otherwise no selected values will be patched below
|
organization,
|
||||||
this.changeDetectorRef.detectChanges();
|
flexibleCollectionsV1Enabled,
|
||||||
|
]) => {
|
||||||
this.groupForm.patchValue({
|
this.members = members;
|
||||||
name: this.group.name,
|
this.group = group;
|
||||||
externalId: this.group.externalId,
|
this.collections = mapToAccessItemViews(
|
||||||
accessAll: this.group.accessAll,
|
collections,
|
||||||
members: this.group.members.map((m) => ({
|
organization,
|
||||||
id: m,
|
flexibleCollectionsV1Enabled,
|
||||||
type: AccessItemType.Member,
|
group,
|
||||||
})),
|
|
||||||
collections: this.group.collections.map((gc) => ({
|
|
||||||
id: gc.id,
|
|
||||||
type: AccessItemType.Collection,
|
|
||||||
permission: convertToPermission(gc),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the current user is not already in the group and cannot add themselves, remove them from the list
|
|
||||||
if (restrictGroupAccess) {
|
|
||||||
const organizationUserId = this.members.find((m) => m.userId === activeAccount.id).id;
|
|
||||||
const isAlreadyInGroup = this.groupForm.value.members.some(
|
|
||||||
(m) => m.id === organizationUserId,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAlreadyInGroup) {
|
if (this.group != undefined) {
|
||||||
this.members = this.members.filter((m) => m.id !== organizationUserId);
|
// Must detect changes so that AccessSelector @Inputs() are aware of the latest
|
||||||
}
|
// collections/members set above, otherwise no selected values will be patched below
|
||||||
}
|
this.changeDetectorRef.detectChanges();
|
||||||
|
|
||||||
this.loading = false;
|
this.groupForm.patchValue({
|
||||||
});
|
name: this.group.name,
|
||||||
|
externalId: this.group.externalId,
|
||||||
|
accessAll: this.group.accessAll,
|
||||||
|
members: this.group.members.map((m) => ({
|
||||||
|
id: m,
|
||||||
|
type: AccessItemType.Member,
|
||||||
|
})),
|
||||||
|
collections: mapToAccessSelections(group, this.collections),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the current user is not already in the group and cannot add themselves, remove them from the list
|
||||||
|
if (restrictGroupAccess) {
|
||||||
|
const organizationUserId = this.members.find((m) => m.userId === activeAccount.id).id;
|
||||||
|
const isAlreadyInGroup = this.groupForm.value.members.some(
|
||||||
|
(m) => m.id === organizationUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isAlreadyInGroup) {
|
||||||
|
this.members = this.members.filter((m) => m.id !== organizationUserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@@ -355,3 +355,46 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||||||
this.dialogRef.close(GroupAddEditDialogResultType.Deleted);
|
this.dialogRef.close(GroupAddEditDialogResultType.Deleted);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the group's current collection access to AccessItemValues to populate the access-selector's FormControl
|
||||||
|
*/
|
||||||
|
function mapToAccessSelections(group: GroupView, items: AccessItemView[]): AccessItemValue[] {
|
||||||
|
return (
|
||||||
|
group.collections
|
||||||
|
// The FormControl value only represents editable collection access - exclude readonly access selections
|
||||||
|
.filter((selection) => !items.find((item) => item.id == selection.id).readonly)
|
||||||
|
.map((gc) => ({
|
||||||
|
id: gc.id,
|
||||||
|
type: AccessItemType.Collection,
|
||||||
|
permission: convertToPermission(gc),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the organization's collections to AccessItemViews to populate the access-selector's multi-select
|
||||||
|
*/
|
||||||
|
function mapToAccessItemViews(
|
||||||
|
collections: CollectionAdminView[],
|
||||||
|
organization: Organization,
|
||||||
|
flexibleCollectionsV1Enabled: boolean,
|
||||||
|
group?: GroupView,
|
||||||
|
): AccessItemView[] {
|
||||||
|
return (
|
||||||
|
collections
|
||||||
|
.map<AccessItemView>((c) => {
|
||||||
|
const accessSelection = group?.collections.find((access) => access.id == c.id) ?? undefined;
|
||||||
|
return {
|
||||||
|
id: c.id,
|
||||||
|
type: AccessItemType.Collection,
|
||||||
|
labelName: c.name,
|
||||||
|
listName: c.name,
|
||||||
|
readonly: !c.canEditGroupAccess(organization, flexibleCollectionsV1Enabled),
|
||||||
|
readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
// Remove any collection views that are not already assigned and that we don't have permissions to assign access to
|
||||||
|
.filter((item) => !item.readonly || group?.collections.some((access) => access.id == item.id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router";
|
|||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@@ -27,11 +28,20 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
|
|||||||
organizationService: OrganizationService,
|
organizationService: OrganizationService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(cipherService, auditService, organizationService, modalService, passwordRepromptService);
|
super(
|
||||||
|
cipherService,
|
||||||
|
auditService,
|
||||||
|
organizationService,
|
||||||
|
modalService,
|
||||||
|
passwordRepromptService,
|
||||||
|
i18nService,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.isAdminConsoleActive = true;
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.organization = await this.organizationService.get(params.organizationId);
|
this.organization = await this.organizationService.get(params.organizationId);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@@ -24,11 +25,20 @@ export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorRepor
|
|||||||
logService: LogService,
|
logService: LogService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
organizationService: OrganizationService,
|
organizationService: OrganizationService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(cipherService, organizationService, modalService, logService, passwordRepromptService);
|
super(
|
||||||
|
cipherService,
|
||||||
|
organizationService,
|
||||||
|
modalService,
|
||||||
|
logService,
|
||||||
|
passwordRepromptService,
|
||||||
|
i18nService,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.isAdminConsoleActive = true;
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.organization = await this.organizationService.get(params.organizationId);
|
this.organization = await this.organizationService.get(params.organizationId);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@@ -25,11 +26,13 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
organizationService: OrganizationService,
|
organizationService: OrganizationService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(cipherService, organizationService, modalService, passwordRepromptService);
|
super(cipherService, organizationService, modalService, passwordRepromptService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.isAdminConsoleActive = true;
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.organization = await this.organizationService.get(params.organizationId);
|
this.organization = await this.organizationService.get(params.organizationId);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
@@ -22,11 +23,13 @@ export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesRepor
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
organizationService: OrganizationService,
|
organizationService: OrganizationService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(cipherService, organizationService, modalService, passwordRepromptService);
|
super(cipherService, organizationService, modalService, passwordRepromptService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.isAdminConsoleActive = true;
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.organization = await this.organizationService.get(params.organizationId);
|
this.organization = await this.organizationService.get(params.organizationId);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
@@ -27,6 +28,7 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
organizationService: OrganizationService,
|
organizationService: OrganizationService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
cipherService,
|
cipherService,
|
||||||
@@ -34,10 +36,12 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone
|
|||||||
organizationService,
|
organizationService,
|
||||||
modalService,
|
modalService,
|
||||||
passwordRepromptService,
|
passwordRepromptService,
|
||||||
|
i18nService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.isAdminConsoleActive = true;
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.organization = await this.organizationService.get(params.organizationId);
|
this.organization = await this.organizationService.get(params.organizationId);
|
||||||
|
|||||||
@@ -21,9 +21,10 @@
|
|||||||
ariaCurrentWhenActive="page"
|
ariaCurrentWhenActive="page"
|
||||||
>
|
>
|
||||||
<i class="bwi {{ product.icon }} tw-text-4xl !tw-m-0 !tw-mb-1"></i>
|
<i class="bwi {{ product.icon }} tw-text-4xl !tw-m-0 !tw-mb-1"></i>
|
||||||
<span class="tw-text-center tw-text-sm tw-leading-snug group-hover:tw-underline">{{
|
<span
|
||||||
product.name
|
class="tw-max-w-24 tw-text-center tw-text-sm tw-leading-snug group-hover:tw-underline"
|
||||||
}}</span>
|
>{{ product.name }}</span
|
||||||
|
>
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Component, ViewChild } from "@angular/core";
|
import { Component, ViewChild } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, ParamMap, Router } from "@angular/router";
|
||||||
import { combineLatest, concatMap } from "rxjs";
|
import { combineLatest, concatMap, map } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
canAccessOrgAdmin,
|
canAccessOrgAdmin,
|
||||||
OrganizationService,
|
OrganizationService,
|
||||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { MenuComponent } from "@bitwarden/components";
|
import { MenuComponent } from "@bitwarden/components";
|
||||||
|
|
||||||
type ProductSwitcherItem = {
|
type ProductSwitcherItem = {
|
||||||
@@ -48,6 +49,13 @@ export class ProductSwitcherContentComponent {
|
|||||||
this.organizationService.organizations$,
|
this.organizationService.organizations$,
|
||||||
this.route.paramMap,
|
this.route.paramMap,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
|
map(([orgs, paramMap]): [Organization[], ParamMap] => {
|
||||||
|
return [
|
||||||
|
// Sort orgs by name to match the order within the sidebar
|
||||||
|
orgs.sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
|
paramMap,
|
||||||
|
];
|
||||||
|
}),
|
||||||
concatMap(async ([orgs, paramMap]) => {
|
concatMap(async ([orgs, paramMap]) => {
|
||||||
const routeOrg = orgs.find((o) => o.id === paramMap.get("organizationId"));
|
const routeOrg = orgs.find((o) => o.id === paramMap.get("organizationId"));
|
||||||
// If the active route org doesn't have access to SM, find the first org that does.
|
// If the active route org doesn't have access to SM, find the first org that does.
|
||||||
@@ -89,7 +97,7 @@ export class ProductSwitcherContentComponent {
|
|||||||
},
|
},
|
||||||
ac: {
|
ac: {
|
||||||
name: "Admin Console",
|
name: "Admin Console",
|
||||||
icon: "bwi-business",
|
icon: "bwi-user-monitor",
|
||||||
appRoute: ["/organizations", acOrg?.id],
|
appRoute: ["/organizations", acOrg?.id],
|
||||||
marketingRoute: "https://bitwarden.com/products/business/",
|
marketingRoute: "https://bitwarden.com/products/business/",
|
||||||
isActive: this.router.url.includes("/organizations/"),
|
isActive: this.router.url.includes("/organizations/"),
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Directive, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Directive, ViewChild, ViewContainerRef, OnDestroy } from "@angular/core";
|
||||||
import { Observable } from "rxjs";
|
import { BehaviorSubject, Observable, Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
@@ -12,27 +14,111 @@ import { AddEditComponent } from "../../../vault/individual-vault/add-edit.compo
|
|||||||
import { AddEditComponent as OrgAddEditComponent } from "../../../vault/org-vault/add-edit.component";
|
import { AddEditComponent as OrgAddEditComponent } from "../../../vault/org-vault/add-edit.component";
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class CipherReportComponent {
|
export class CipherReportComponent implements OnDestroy {
|
||||||
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
|
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
|
||||||
cipherAddEditModalRef: ViewContainerRef;
|
cipherAddEditModalRef: ViewContainerRef;
|
||||||
|
isAdminConsoleActive = false;
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
hasLoaded = false;
|
hasLoaded = false;
|
||||||
ciphers: CipherView[] = [];
|
ciphers: CipherView[] = [];
|
||||||
|
allCiphers: CipherView[] = [];
|
||||||
organization: Organization;
|
organization: Organization;
|
||||||
|
organizations: Organization[];
|
||||||
organizations$: Observable<Organization[]>;
|
organizations$: Observable<Organization[]>;
|
||||||
|
|
||||||
|
filterStatus: any = [0];
|
||||||
|
showFilterToggle: boolean = false;
|
||||||
|
vaultMsg: string = "vault";
|
||||||
|
currentFilterStatus: number | string;
|
||||||
|
protected filterOrgStatus$ = new BehaviorSubject<number | string>(0);
|
||||||
|
private destroyed$: Subject<void> = new Subject();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
protected cipherService: CipherService,
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
protected passwordRepromptService: PasswordRepromptService,
|
protected passwordRepromptService: PasswordRepromptService,
|
||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
this.organizations$ = this.organizationService.organizations$;
|
this.organizations$ = this.organizationService.organizations$;
|
||||||
|
this.organizations$.pipe(takeUntil(this.destroyed$)).subscribe((orgs) => {
|
||||||
|
this.organizations = orgs;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroyed$.next();
|
||||||
|
this.destroyed$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(filterId: string | number) {
|
||||||
|
let orgName: any;
|
||||||
|
|
||||||
|
if (filterId === 0) {
|
||||||
|
orgName = this.i18nService.t("all");
|
||||||
|
} else if (filterId === 1) {
|
||||||
|
orgName = this.i18nService.t("me");
|
||||||
|
} else {
|
||||||
|
this.organizations.filter((org: Organization) => {
|
||||||
|
if (org.id === filterId) {
|
||||||
|
orgName = org.name;
|
||||||
|
return org;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return orgName;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCount(filterId: string | number) {
|
||||||
|
let orgFilterStatus: any;
|
||||||
|
let cipherCount;
|
||||||
|
|
||||||
|
if (filterId === 0) {
|
||||||
|
cipherCount = this.allCiphers.length;
|
||||||
|
} else if (filterId === 1) {
|
||||||
|
cipherCount = this.allCiphers.filter((c: any) => c.orgFilterStatus === null).length;
|
||||||
|
} else {
|
||||||
|
this.organizations.filter((org: Organization) => {
|
||||||
|
if (org.id === filterId) {
|
||||||
|
orgFilterStatus = org.id;
|
||||||
|
return org;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cipherCount = this.allCiphers.filter(
|
||||||
|
(c: any) => c.orgFilterStatus === orgFilterStatus,
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
return cipherCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
async filterOrgToggle(status: any) {
|
||||||
|
this.currentFilterStatus = status;
|
||||||
|
await this.setCiphers();
|
||||||
|
if (status === 0) {
|
||||||
|
return;
|
||||||
|
} else if (status === 1) {
|
||||||
|
this.ciphers = this.ciphers.filter((c: any) => c.orgFilterStatus == null);
|
||||||
|
} else {
|
||||||
|
this.ciphers = this.ciphers.filter((c: any) => c.orgFilterStatus === status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
await this.setCiphers();
|
// when a user fixes an item in a report we want to persist the filter they had
|
||||||
|
// if they fix the last item of that filter we will go back to the "All" filter
|
||||||
|
if (this.currentFilterStatus) {
|
||||||
|
if (this.ciphers.length > 2) {
|
||||||
|
this.filterOrgStatus$.next(this.currentFilterStatus);
|
||||||
|
await this.filterOrgToggle(this.currentFilterStatus);
|
||||||
|
} else {
|
||||||
|
this.filterOrgStatus$.next(0);
|
||||||
|
await this.filterOrgToggle(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.setCiphers();
|
||||||
|
}
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.hasLoaded = true;
|
this.hasLoaded = true;
|
||||||
}
|
}
|
||||||
@@ -76,7 +162,7 @@ export class CipherReportComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async setCiphers() {
|
protected async setCiphers() {
|
||||||
this.ciphers = [];
|
this.allCiphers = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async repromptCipher(c: CipherView) {
|
protected async repromptCipher(c: CipherView) {
|
||||||
@@ -85,4 +171,32 @@ export class CipherReportComponent {
|
|||||||
(await this.passwordRepromptService.showPasswordPrompt())
|
(await this.passwordRepromptService.showPasswordPrompt())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async getAllCiphers(): Promise<CipherView[]> {
|
||||||
|
return await this.cipherService.getAllDecrypted();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected filterCiphersByOrg(ciphersList: CipherView[]) {
|
||||||
|
this.allCiphers = [...ciphersList];
|
||||||
|
|
||||||
|
this.ciphers = ciphersList.map((ciph: any) => {
|
||||||
|
ciph.orgFilterStatus = ciph.organizationId;
|
||||||
|
|
||||||
|
if (this.filterStatus.indexOf(ciph.organizationId) === -1 && ciph.organizationId != null) {
|
||||||
|
this.filterStatus.push(ciph.organizationId);
|
||||||
|
} else if (this.filterStatus.indexOf(1) === -1 && ciph.organizationId == null) {
|
||||||
|
this.filterStatus.splice(1, 0, 1);
|
||||||
|
}
|
||||||
|
return ciph;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.filterStatus.length > 2) {
|
||||||
|
this.showFilterToggle = true;
|
||||||
|
this.vaultMsg = "vaults";
|
||||||
|
} else {
|
||||||
|
// If a user fixes an item and there is only one item left remove the filter toggle and change the vault message to singular
|
||||||
|
this.showFilterToggle = false;
|
||||||
|
this.vaultMsg = "vault";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,32 @@
|
|||||||
</app-callout>
|
</app-callout>
|
||||||
<ng-container *ngIf="ciphers.length">
|
<ng-container *ngIf="ciphers.length">
|
||||||
<app-callout type="danger" title="{{ 'exposedPasswordsFound' | i18n }}" [useAlertRole]="true">
|
<app-callout type="danger" title="{{ 'exposedPasswordsFound' | i18n }}" [useAlertRole]="true">
|
||||||
{{ "exposedPasswordsFoundDesc" | i18n: (ciphers.length | number) }}
|
{{ "exposedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
<bit-toggle-group
|
||||||
|
*ngIf="showFilterToggle && !isAdminConsoleActive"
|
||||||
|
[selected]="filterOrgStatus$ | async"
|
||||||
|
(selectedChange)="filterOrgToggle($event)"
|
||||||
|
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||||
|
>
|
||||||
|
<ng-container *ngFor="let status of filterStatus">
|
||||||
|
<bit-toggle [value]="status">
|
||||||
|
{{ getName(status) }}
|
||||||
|
<span bitBadge variant="info"> {{ getCount(status) }} </span>
|
||||||
|
</bit-toggle>
|
||||||
|
</ng-container>
|
||||||
|
</bit-toggle-group>
|
||||||
<table class="table table-hover table-list table-ciphers">
|
<table class="table table-hover table-list table-ciphers">
|
||||||
|
<thead
|
||||||
|
class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted"
|
||||||
|
*ngIf="!isAdminConsoleActive"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>{{ "name" | i18n }}</th>
|
||||||
|
<th>{{ "owner" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let c of ciphers">
|
<tr *ngFor="let c of ciphers">
|
||||||
<td class="table-list-icon">
|
<td class="table-list-icon">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@@ -17,9 +18,12 @@ describe("ExposedPasswordsReportComponent", () => {
|
|||||||
let component: ExposedPasswordsReportComponent;
|
let component: ExposedPasswordsReportComponent;
|
||||||
let fixture: ComponentFixture<ExposedPasswordsReportComponent>;
|
let fixture: ComponentFixture<ExposedPasswordsReportComponent>;
|
||||||
let auditService: MockProxy<AuditService>;
|
let auditService: MockProxy<AuditService>;
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
auditService = mock<AuditService>();
|
auditService = mock<AuditService>();
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
organizationService.organizations$ = of([]);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -35,7 +39,7 @@ describe("ExposedPasswordsReportComponent", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: OrganizationService,
|
provide: OrganizationService,
|
||||||
useValue: mock<OrganizationService>(),
|
useValue: organizationService,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ModalService,
|
provide: ModalService,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@@ -24,8 +25,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
|
|||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(modalService, passwordRepromptService, organizationService);
|
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -36,7 +38,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
|
|||||||
const allCiphers = await this.getAllCiphers();
|
const allCiphers = await this.getAllCiphers();
|
||||||
const exposedPasswordCiphers: CipherView[] = [];
|
const exposedPasswordCiphers: CipherView[] = [];
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
allCiphers.forEach((ciph) => {
|
this.filterStatus = [0];
|
||||||
|
|
||||||
|
allCiphers.forEach((ciph: any) => {
|
||||||
const { type, login, isDeleted, edit, viewPassword, id } = ciph;
|
const { type, login, isDeleted, edit, viewPassword, id } = ciph;
|
||||||
if (
|
if (
|
||||||
type !== CipherType.Login ||
|
type !== CipherType.Login ||
|
||||||
@@ -48,6 +52,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => {
|
const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => {
|
||||||
if (exposedCount > 0) {
|
if (exposedCount > 0) {
|
||||||
exposedPasswordCiphers.push(ciph);
|
exposedPasswordCiphers.push(ciph);
|
||||||
@@ -57,11 +62,8 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
|
|||||||
promises.push(promise);
|
promises.push(promise);
|
||||||
});
|
});
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
this.ciphers = [...exposedPasswordCiphers];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getAllCiphers(): Promise<CipherView[]> {
|
this.filterCiphersByOrg(exposedPasswordCiphers);
|
||||||
return this.cipherService.getAllDecrypted();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected canManageCipher(c: CipherView): boolean {
|
protected canManageCipher(c: CipherView): boolean {
|
||||||
|
|||||||
@@ -16,9 +16,32 @@
|
|||||||
</app-callout>
|
</app-callout>
|
||||||
<ng-container *ngIf="ciphers.length">
|
<ng-container *ngIf="ciphers.length">
|
||||||
<app-callout type="danger" title="{{ 'inactive2faFound' | i18n }}">
|
<app-callout type="danger" title="{{ 'inactive2faFound' | i18n }}">
|
||||||
{{ "inactive2faFoundDesc" | i18n: (ciphers.length | number) }}
|
{{ "inactive2faFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
<bit-toggle-group
|
||||||
|
*ngIf="showFilterToggle && !isAdminConsoleActive"
|
||||||
|
[selected]="filterOrgStatus$ | async"
|
||||||
|
(selectedChange)="filterOrgToggle($event)"
|
||||||
|
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||||
|
>
|
||||||
|
<ng-container *ngFor="let status of filterStatus">
|
||||||
|
<bit-toggle [value]="status">
|
||||||
|
{{ getName(status) }}
|
||||||
|
<span bitBadge variant="info"> {{ getCount(status) }} </span>
|
||||||
|
</bit-toggle>
|
||||||
|
</ng-container>
|
||||||
|
</bit-toggle-group>
|
||||||
<table class="table table-hover table-list table-ciphers">
|
<table class="table table-hover table-list table-ciphers">
|
||||||
|
<thead
|
||||||
|
class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted"
|
||||||
|
*ngIf="!isAdminConsoleActive"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>{{ "name" | i18n }}</th>
|
||||||
|
<th>{{ "owner" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let c of ciphers">
|
<tr *ngFor="let c of ciphers">
|
||||||
<td class="table-list-icon">
|
<td class="table-list-icon">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@@ -16,8 +17,11 @@ import { cipherData } from "./reports-ciphers.mock";
|
|||||||
describe("InactiveTwoFactorReportComponent", () => {
|
describe("InactiveTwoFactorReportComponent", () => {
|
||||||
let component: InactiveTwoFactorReportComponent;
|
let component: InactiveTwoFactorReportComponent;
|
||||||
let fixture: ComponentFixture<InactiveTwoFactorReportComponent>;
|
let fixture: ComponentFixture<InactiveTwoFactorReportComponent>;
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
organizationService.organizations$ = of([]);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -29,7 +33,7 @@ describe("InactiveTwoFactorReportComponent", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: OrganizationService,
|
provide: OrganizationService,
|
||||||
useValue: mock<OrganizationService>(),
|
useValue: organizationService,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ModalService,
|
provide: ModalService,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@@ -26,8 +27,9 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
|||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(modalService, passwordRepromptService, organizationService);
|
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -45,6 +47,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
|||||||
const allCiphers = await this.getAllCiphers();
|
const allCiphers = await this.getAllCiphers();
|
||||||
const inactive2faCiphers: CipherView[] = [];
|
const inactive2faCiphers: CipherView[] = [];
|
||||||
const docs = new Map<string, string>();
|
const docs = new Map<string, string>();
|
||||||
|
this.filterStatus = [0];
|
||||||
|
|
||||||
allCiphers.forEach((ciph) => {
|
allCiphers.forEach((ciph) => {
|
||||||
const { type, login, isDeleted, edit, id, viewPassword } = ciph;
|
const { type, login, isDeleted, edit, id, viewPassword } = ciph;
|
||||||
@@ -58,6 +61,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < login.uris.length; i++) {
|
for (let i = 0; i < login.uris.length; i++) {
|
||||||
const u = login.uris[i];
|
const u = login.uris[i];
|
||||||
if (u.uri != null && u.uri !== "") {
|
if (u.uri != null && u.uri !== "") {
|
||||||
@@ -75,15 +79,12 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.ciphers = [...inactive2faCiphers];
|
|
||||||
|
this.filterCiphersByOrg(inactive2faCiphers);
|
||||||
this.cipherDocs = docs;
|
this.cipherDocs = docs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getAllCiphers(): Promise<CipherView[]> {
|
|
||||||
return this.cipherService.getAllDecrypted();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async load2fa() {
|
private async load2fa() {
|
||||||
if (this.services.size > 0) {
|
if (this.services.size > 0) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -16,9 +16,34 @@
|
|||||||
</app-callout>
|
</app-callout>
|
||||||
<ng-container *ngIf="ciphers.length">
|
<ng-container *ngIf="ciphers.length">
|
||||||
<app-callout type="danger" title="{{ 'reusedPasswordsFound' | i18n }}">
|
<app-callout type="danger" title="{{ 'reusedPasswordsFound' | i18n }}">
|
||||||
{{ "reusedPasswordsFoundDesc" | i18n: (ciphers.length | number) }}
|
{{ "reusedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
|
||||||
|
<bit-toggle-group
|
||||||
|
*ngIf="showFilterToggle && !isAdminConsoleActive"
|
||||||
|
[selected]="filterOrgStatus$ | async"
|
||||||
|
(selectedChange)="filterOrgToggle($event)"
|
||||||
|
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||||
|
>
|
||||||
|
<ng-container *ngFor="let status of filterStatus">
|
||||||
|
<bit-toggle [value]="status">
|
||||||
|
{{ getName(status) }}
|
||||||
|
<span bitBadge variant="info"> {{ getCount(status) }} </span>
|
||||||
|
</bit-toggle>
|
||||||
|
</ng-container>
|
||||||
|
</bit-toggle-group>
|
||||||
|
|
||||||
<table class="table table-hover table-list table-ciphers">
|
<table class="table table-hover table-list table-ciphers">
|
||||||
|
<thead
|
||||||
|
class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted"
|
||||||
|
*ngIf="!isAdminConsoleActive"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>{{ "name" | i18n }}</th>
|
||||||
|
<th>{{ "owner" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let c of ciphers">
|
<tr *ngFor="let c of ciphers">
|
||||||
<td class="table-list-icon">
|
<td class="table-list-icon">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@@ -15,8 +16,11 @@ import { ReusedPasswordsReportComponent } from "./reused-passwords-report.compon
|
|||||||
describe("ReusedPasswordsReportComponent", () => {
|
describe("ReusedPasswordsReportComponent", () => {
|
||||||
let component: ReusedPasswordsReportComponent;
|
let component: ReusedPasswordsReportComponent;
|
||||||
let fixture: ComponentFixture<ReusedPasswordsReportComponent>;
|
let fixture: ComponentFixture<ReusedPasswordsReportComponent>;
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
organizationService.organizations$ = of([]);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -28,7 +32,7 @@ describe("ReusedPasswordsReportComponent", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: OrganizationService,
|
provide: OrganizationService,
|
||||||
useValue: mock<OrganizationService>(),
|
useValue: organizationService,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ModalService,
|
provide: ModalService,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@@ -22,8 +23,9 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
|
|||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(modalService, passwordRepromptService, organizationService);
|
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -34,6 +36,8 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
|
|||||||
const allCiphers = await this.getAllCiphers();
|
const allCiphers = await this.getAllCiphers();
|
||||||
const ciphersWithPasswords: CipherView[] = [];
|
const ciphersWithPasswords: CipherView[] = [];
|
||||||
this.passwordUseMap = new Map<string, number>();
|
this.passwordUseMap = new Map<string, number>();
|
||||||
|
this.filterStatus = [0];
|
||||||
|
|
||||||
allCiphers.forEach((ciph) => {
|
allCiphers.forEach((ciph) => {
|
||||||
const { type, login, isDeleted, edit, viewPassword } = ciph;
|
const { type, login, isDeleted, edit, viewPassword } = ciph;
|
||||||
if (
|
if (
|
||||||
@@ -46,6 +50,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ciphersWithPasswords.push(ciph);
|
ciphersWithPasswords.push(ciph);
|
||||||
if (this.passwordUseMap.has(login.password)) {
|
if (this.passwordUseMap.has(login.password)) {
|
||||||
this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) + 1);
|
this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) + 1);
|
||||||
@@ -57,11 +62,8 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
|
|||||||
(c) =>
|
(c) =>
|
||||||
this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password) > 1,
|
this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password) > 1,
|
||||||
);
|
);
|
||||||
this.ciphers = reusedPasswordCiphers;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getAllCiphers(): Promise<CipherView[]> {
|
this.filterCiphersByOrg(reusedPasswordCiphers);
|
||||||
return this.cipherService.getAllDecrypted();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected canManageCipher(c: CipherView): boolean {
|
protected canManageCipher(c: CipherView): boolean {
|
||||||
|
|||||||
@@ -16,9 +16,33 @@
|
|||||||
</app-callout>
|
</app-callout>
|
||||||
<ng-container *ngIf="ciphers.length">
|
<ng-container *ngIf="ciphers.length">
|
||||||
<app-callout type="danger" title="{{ 'unsecuredWebsitesFound' | i18n }}">
|
<app-callout type="danger" title="{{ 'unsecuredWebsitesFound' | i18n }}">
|
||||||
{{ "unsecuredWebsitesFoundDesc" | i18n: (ciphers.length | number) }}
|
{{ "unsecuredWebsitesFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
|
||||||
|
<bit-toggle-group
|
||||||
|
*ngIf="showFilterToggle && !isAdminConsoleActive"
|
||||||
|
[selected]="filterOrgStatus$ | async"
|
||||||
|
(selectedChange)="filterOrgToggle($event)"
|
||||||
|
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||||
|
>
|
||||||
|
<ng-container *ngFor="let status of filterStatus">
|
||||||
|
<bit-toggle [value]="status">
|
||||||
|
{{ getName(status) }}
|
||||||
|
<span bitBadge variant="info"> {{ getCount(status) }} </span>
|
||||||
|
</bit-toggle>
|
||||||
|
</ng-container>
|
||||||
|
</bit-toggle-group>
|
||||||
<table class="table table-hover table-list table-ciphers">
|
<table class="table table-hover table-list table-ciphers">
|
||||||
|
<thead
|
||||||
|
class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted"
|
||||||
|
*ngIf="!isAdminConsoleActive"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>{{ "name" | i18n }}</th>
|
||||||
|
<th>{{ "owner" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let c of ciphers">
|
<tr *ngFor="let c of ciphers">
|
||||||
<td class="table-list-icon">
|
<td class="table-list-icon">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@@ -15,8 +16,11 @@ import { UnsecuredWebsitesReportComponent } from "./unsecured-websites-report.co
|
|||||||
describe("UnsecuredWebsitesReportComponent", () => {
|
describe("UnsecuredWebsitesReportComponent", () => {
|
||||||
let component: UnsecuredWebsitesReportComponent;
|
let component: UnsecuredWebsitesReportComponent;
|
||||||
let fixture: ComponentFixture<UnsecuredWebsitesReportComponent>;
|
let fixture: ComponentFixture<UnsecuredWebsitesReportComponent>;
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
organizationService.organizations$ = of([]);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -28,7 +32,7 @@ describe("UnsecuredWebsitesReportComponent", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: OrganizationService,
|
provide: OrganizationService,
|
||||||
useValue: mock<OrganizationService>(),
|
useValue: organizationService,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ModalService,
|
provide: ModalService,
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { CipherReportComponent } from "./cipher-report.component";
|
import { CipherReportComponent } from "./cipher-report.component";
|
||||||
@@ -21,8 +21,9 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl
|
|||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(modalService, passwordRepromptService, organizationService);
|
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -31,18 +32,15 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl
|
|||||||
|
|
||||||
async setCiphers() {
|
async setCiphers() {
|
||||||
const allCiphers = await this.getAllCiphers();
|
const allCiphers = await this.getAllCiphers();
|
||||||
|
this.filterStatus = [0];
|
||||||
const unsecuredCiphers = allCiphers.filter((c) => {
|
const unsecuredCiphers = allCiphers.filter((c) => {
|
||||||
if (c.type !== CipherType.Login || !c.login.hasUris || c.isDeleted) {
|
if (c.type !== CipherType.Login || !c.login.hasUris || c.isDeleted) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return c.login.uris.some((u) => u.uri != null && u.uri.indexOf("http://") === 0);
|
|
||||||
});
|
|
||||||
this.ciphers = unsecuredCiphers.filter(
|
|
||||||
(c) => (!this.organization && c.edit) || (this.organization && !c.edit),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getAllCiphers(): Promise<CipherView[]> {
|
return c.login.uris.some((u: any) => u.uri != null && u.uri.indexOf("http://") === 0);
|
||||||
return this.cipherService.getAllDecrypted();
|
});
|
||||||
|
|
||||||
|
this.filterCiphersByOrg(unsecuredCiphers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,32 @@
|
|||||||
</app-callout>
|
</app-callout>
|
||||||
<ng-container *ngIf="ciphers.length">
|
<ng-container *ngIf="ciphers.length">
|
||||||
<app-callout type="danger" title="{{ 'weakPasswordsFound' | i18n }}">
|
<app-callout type="danger" title="{{ 'weakPasswordsFound' | i18n }}">
|
||||||
{{ "weakPasswordsFoundDesc" | i18n: (ciphers.length | number) }}
|
{{ "weakPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
<bit-toggle-group
|
||||||
|
*ngIf="showFilterToggle && !isAdminConsoleActive"
|
||||||
|
[selected]="filterOrgStatus$ | async"
|
||||||
|
(selectedChange)="filterOrgToggle($event)"
|
||||||
|
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||||
|
>
|
||||||
|
<ng-container *ngFor="let status of filterStatus">
|
||||||
|
<bit-toggle [value]="status">
|
||||||
|
{{ getName(status) }}
|
||||||
|
<span bitBadge variant="info"> {{ getCount(status) }} </span>
|
||||||
|
</bit-toggle>
|
||||||
|
</ng-container>
|
||||||
|
</bit-toggle-group>
|
||||||
<table class="table table-hover table-list table-ciphers">
|
<table class="table table-hover table-list table-ciphers">
|
||||||
|
<thead
|
||||||
|
class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted"
|
||||||
|
*ngIf="!isAdminConsoleActive"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>{{ "name" | i18n }}</th>
|
||||||
|
<th>{{ "owner" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let c of ciphers">
|
<tr *ngFor="let c of ciphers">
|
||||||
<td class="table-list-icon">
|
<td class="table-list-icon">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@@ -17,9 +18,12 @@ describe("WeakPasswordsReportComponent", () => {
|
|||||||
let component: WeakPasswordsReportComponent;
|
let component: WeakPasswordsReportComponent;
|
||||||
let fixture: ComponentFixture<WeakPasswordsReportComponent>;
|
let fixture: ComponentFixture<WeakPasswordsReportComponent>;
|
||||||
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
|
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
passwordStrengthService = mock<PasswordStrengthServiceAbstraction>();
|
passwordStrengthService = mock<PasswordStrengthServiceAbstraction>();
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
organizationService.organizations$ = of([]);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -35,7 +39,7 @@ describe("WeakPasswordsReportComponent", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: OrganizationService,
|
provide: OrganizationService,
|
||||||
useValue: mock<OrganizationService>(),
|
useValue: organizationService,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ModalService,
|
provide: ModalService,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@@ -29,8 +30,9 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(modalService, passwordRepromptService, organizationService);
|
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -38,7 +40,10 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setCiphers() {
|
async setCiphers() {
|
||||||
const allCiphers = await this.getAllCiphers();
|
const allCiphers: any = await this.getAllCiphers();
|
||||||
|
this.passwordStrengthCache = new Map<string, number>();
|
||||||
|
this.weakPasswordCiphers = [];
|
||||||
|
this.filterStatus = [0];
|
||||||
this.findWeakPasswords(allCiphers);
|
this.findWeakPasswords(allCiphers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +60,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasUserName = this.isUserNameNotEmpty(ciph);
|
const hasUserName = this.isUserNameNotEmpty(ciph);
|
||||||
const cacheKey = this.getCacheKey(ciph);
|
const cacheKey = this.getCacheKey(ciph);
|
||||||
if (!this.passwordStrengthCache.has(cacheKey)) {
|
if (!this.passwordStrengthCache.has(cacheKey)) {
|
||||||
@@ -87,6 +93,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||||||
this.passwordStrengthCache.set(cacheKey, result.score);
|
this.passwordStrengthCache.set(cacheKey, result.score);
|
||||||
}
|
}
|
||||||
const score = this.passwordStrengthCache.get(cacheKey);
|
const score = this.passwordStrengthCache.get(cacheKey);
|
||||||
|
|
||||||
if (score != null && score <= 2) {
|
if (score != null && score <= 2) {
|
||||||
this.passwordStrengthMap.set(id, this.scoreKey(score));
|
this.passwordStrengthMap.set(id, this.scoreKey(score));
|
||||||
this.weakPasswordCiphers.push(ciph);
|
this.weakPasswordCiphers.push(ciph);
|
||||||
@@ -98,11 +105,8 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||||||
this.passwordStrengthCache.get(this.getCacheKey(b))
|
this.passwordStrengthCache.get(this.getCacheKey(b))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.ciphers = [...this.weakPasswordCiphers];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getAllCiphers(): Promise<CipherView[]> {
|
this.filterCiphersByOrg(this.weakPasswordCiphers);
|
||||||
return this.cipherService.getAllDecrypted();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected canManageCipher(c: CipherView): boolean {
|
protected canManageCipher(c: CipherView): boolean {
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export class VaultItemsComponent {
|
|||||||
@Input() showCollections: boolean;
|
@Input() showCollections: boolean;
|
||||||
@Input() showGroups: boolean;
|
@Input() showGroups: boolean;
|
||||||
@Input() useEvents: boolean;
|
@Input() useEvents: boolean;
|
||||||
@Input() cloneableOrganizationCiphers: boolean;
|
|
||||||
@Input() showPremiumFeatures: boolean;
|
@Input() showPremiumFeatures: boolean;
|
||||||
@Input() showBulkMove: boolean;
|
@Input() showBulkMove: boolean;
|
||||||
@Input() showBulkTrashOptions: boolean;
|
@Input() showBulkTrashOptions: boolean;
|
||||||
@@ -160,10 +159,27 @@ export class VaultItemsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected canClone(vaultItem: VaultItem) {
|
protected canClone(vaultItem: VaultItem) {
|
||||||
return (
|
if (vaultItem.cipher.organizationId == null) {
|
||||||
(vaultItem.cipher.organizationId && this.cloneableOrganizationCiphers) ||
|
return true;
|
||||||
vaultItem.cipher.organizationId == null
|
}
|
||||||
);
|
|
||||||
|
const org = this.allOrganizations.find((o) => o.id === vaultItem.cipher.organizationId);
|
||||||
|
|
||||||
|
// Admins and custom users can always clone in the Org Vault
|
||||||
|
if (this.viewingOrgVault && (org.isAdmin || org.permissions.editAnyCollection)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the cipher belongs to a collection with canManage permission
|
||||||
|
const orgCollections = this.allCollections.filter((c) => c.organizationId === org.id);
|
||||||
|
|
||||||
|
for (const collection of orgCollections) {
|
||||||
|
if (vaultItem.cipher.collectionIds.includes(collection.id) && collection.manage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshItems() {
|
private refreshItems() {
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ export class CollectionAdminView extends CollectionView {
|
|||||||
* Whether the user can modify user access to this collection
|
* Whether the user can modify user access to this collection
|
||||||
*/
|
*/
|
||||||
canEditUserAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
canEditUserAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
||||||
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.canManageUsers;
|
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user can modify group access to this collection
|
||||||
|
*/
|
||||||
|
canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
||||||
|
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageGroups;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@
|
|||||||
[showBulkMove]="showBulkMove"
|
[showBulkMove]="showBulkMove"
|
||||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||||
[useEvents]="false"
|
[useEvents]="false"
|
||||||
[cloneableOrganizationCiphers]="false"
|
|
||||||
[showAdminActions]="false"
|
[showAdminActions]="false"
|
||||||
(onEvent)="onVaultItemsEvent($event)"
|
(onEvent)="onVaultItemsEvent($event)"
|
||||||
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled$ | async"
|
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled$ | async"
|
||||||
|
|||||||
@@ -81,22 +81,6 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected allowOwnershipAssignment() {
|
|
||||||
if (
|
|
||||||
this.ownershipOptions != null &&
|
|
||||||
(this.ownershipOptions.length > 1 || !this.allowPersonal)
|
|
||||||
) {
|
|
||||||
if (this.organization != null) {
|
|
||||||
return (
|
|
||||||
this.cloneMode && this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return !this.editMode || this.cloneMode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadCollections() {
|
protected loadCollections() {
|
||||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||||
return super.loadCollections();
|
return super.loadCollections();
|
||||||
|
|||||||
@@ -48,7 +48,6 @@
|
|||||||
[showBulkMove]="false"
|
[showBulkMove]="false"
|
||||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||||
[useEvents]="organization?.useEvents"
|
[useEvents]="organization?.useEvents"
|
||||||
[cloneableOrganizationCiphers]="true"
|
|
||||||
[showAdminActions]="true"
|
[showAdminActions]="true"
|
||||||
(onEvent)="onVaultItemsEvent($event)"
|
(onEvent)="onVaultItemsEvent($event)"
|
||||||
[showBulkEditCollectionAccess]="organization?.flexibleCollections"
|
[showBulkEditCollectionAccess]="organization?.flexibleCollections"
|
||||||
|
|||||||
@@ -1809,12 +1809,16 @@
|
|||||||
"unsecuredWebsitesFound": {
|
"unsecuredWebsitesFound": {
|
||||||
"message": "Unsecured websites found"
|
"message": "Unsecured websites found"
|
||||||
},
|
},
|
||||||
"unsecuredWebsitesFoundDesc": {
|
"unsecuredWebsitesFoundReportDesc": {
|
||||||
"message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.",
|
"message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {
|
"count": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "8"
|
"example": "8"
|
||||||
|
},
|
||||||
|
"vault": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "this will be 'vault' or 'vaults'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1830,12 +1834,16 @@
|
|||||||
"inactive2faFound": {
|
"inactive2faFound": {
|
||||||
"message": "Logins without two-step login found"
|
"message": "Logins without two-step login found"
|
||||||
},
|
},
|
||||||
"inactive2faFoundDesc": {
|
"inactive2faFoundReportDesc": {
|
||||||
"message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.",
|
"message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {
|
"count": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "8"
|
"example": "8"
|
||||||
|
},
|
||||||
|
"vault": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "this will be 'vault' or 'vaults'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1854,12 +1862,16 @@
|
|||||||
"exposedPasswordsFound": {
|
"exposedPasswordsFound": {
|
||||||
"message": "Exposed passwords found"
|
"message": "Exposed passwords found"
|
||||||
},
|
},
|
||||||
"exposedPasswordsFoundDesc": {
|
"exposedPasswordsFoundReportDesc": {
|
||||||
"message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.",
|
"message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {
|
"count": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "8"
|
"example": "8"
|
||||||
|
},
|
||||||
|
"vault": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "this will be 'vault' or 'vaults'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1887,12 +1899,16 @@
|
|||||||
"weakPasswordsFound": {
|
"weakPasswordsFound": {
|
||||||
"message": "Weak passwords found"
|
"message": "Weak passwords found"
|
||||||
},
|
},
|
||||||
"weakPasswordsFoundDesc": {
|
"weakPasswordsFoundReportDesc": {
|
||||||
"message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.",
|
"message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {
|
"count": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "8"
|
"example": "8"
|
||||||
|
},
|
||||||
|
"vault": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "this will be 'vault' or 'vaults'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1908,12 +1924,16 @@
|
|||||||
"reusedPasswordsFound": {
|
"reusedPasswordsFound": {
|
||||||
"message": "Reused passwords found"
|
"message": "Reused passwords found"
|
||||||
},
|
},
|
||||||
"reusedPasswordsFoundDesc": {
|
"reusedPasswordsFoundReportDesc": {
|
||||||
"message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.",
|
"message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {
|
"count": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "8"
|
"example": "8"
|
||||||
|
},
|
||||||
|
"vault": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "this will be 'vault' or 'vaults'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6460,6 +6480,9 @@
|
|||||||
"editGroupCollectionsDesc": {
|
"editGroupCollectionsDesc": {
|
||||||
"message": "Grant access to collections by adding them to this group."
|
"message": "Grant access to collections by adding them to this group."
|
||||||
},
|
},
|
||||||
|
"editGroupCollectionsRestrictionsDesc": {
|
||||||
|
"message": "You can only assign collections you manage."
|
||||||
|
},
|
||||||
"accessAllCollectionsDesc": {
|
"accessAllCollectionsDesc": {
|
||||||
"message": "Grant access to all current and future collections."
|
"message": "Grant access to all current and future collections."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
|
||||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
@@ -42,6 +42,7 @@ export class AccountComponent {
|
|||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private providerApiService: ProviderApiServiceAbstraction,
|
private providerApiService: ProviderApiServiceAbstraction,
|
||||||
|
private router: Router,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -93,9 +94,8 @@ export class AccountComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.formPromise = this.providerApiService.deleteProvider(this.providerId);
|
|
||||||
try {
|
try {
|
||||||
await this.formPromise;
|
await this.providerApiService.deleteProvider(this.providerId);
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"success",
|
"success",
|
||||||
this.i18nService.t("providerDeleted"),
|
this.i18nService.t("providerDeleted"),
|
||||||
@@ -104,7 +104,8 @@ export class AccountComponent {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
this.formPromise = null;
|
|
||||||
|
await this.router.navigate(["/"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async verifyUser(): Promise<boolean> {
|
private async verifyUser(): Promise<boolean> {
|
||||||
|
|||||||
@@ -58,3 +58,16 @@ export class ServiceAccountPeopleAccessPoliciesView {
|
|||||||
userAccessPolicies: UserServiceAccountAccessPolicyView[];
|
userAccessPolicies: UserServiceAccountAccessPolicyView[];
|
||||||
groupAccessPolicies: GroupServiceAccountAccessPolicyView[];
|
groupAccessPolicies: GroupServiceAccountAccessPolicyView[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ServiceAccountProjectPolicyPermissionDetailsView {
|
||||||
|
accessPolicy: ServiceAccountProjectAccessPolicyView;
|
||||||
|
hasPermission: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ServiceAccountGrantedPoliciesView {
|
||||||
|
grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsView[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProjectServiceAccountsAccessPoliciesView {
|
||||||
|
serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
<div class="tw-w-2/5">
|
<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner">
|
||||||
<p class="tw-mt-8">
|
<div class="tw-w-2/5">
|
||||||
{{ "projectMachineAccountsDescription" | i18n }}
|
<p class="tw-mt-8" *ngIf="!loading">
|
||||||
</p>
|
{{ "projectMachineAccountsDescription" | i18n }}
|
||||||
<sm-access-selector
|
</p>
|
||||||
[rows]="rows$ | async"
|
<sm-access-policy-selector
|
||||||
granteeType="serviceAccounts"
|
[loading]="loading"
|
||||||
[label]="'machineAccounts' | i18n"
|
formControlName="accessPolicies"
|
||||||
[hint]="'projectMachineAccountsSelectHint' | i18n"
|
[addButtonMode]="true"
|
||||||
[columnTitle]="'machineAccounts' | i18n"
|
[items]="items"
|
||||||
[emptyMessage]="'projectEmptyMachineAccountAccessPolicies' | i18n"
|
[label]="'machineAccounts' | i18n"
|
||||||
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)"
|
[hint]="'projectMachineAccountsSelectHint' | i18n"
|
||||||
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)"
|
[columnTitle]="'machineAccounts' | i18n"
|
||||||
(onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)"
|
[emptyMessage]="'projectEmptyMachineAccountAccessPolicies' | i18n"
|
||||||
>
|
>
|
||||||
</sm-access-selector>
|
</sm-access-policy-selector>
|
||||||
</div>
|
<button bitButton buttonType="primary" bitFormButton type="submit" class="tw-mt-7">
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<ng-template #spinner>
|
||||||
|
<div class="tw-items-center tw-justify-center tw-pt-64 tw-text-center">
|
||||||
|
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|||||||
@@ -1,93 +1,69 @@
|
|||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { FormControl, FormGroup } from "@angular/forms";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs";
|
import { combineLatest, Subject, switchMap, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
import { SelectItemView } from "@bitwarden/components";
|
|
||||||
|
|
||||||
|
import { ProjectServiceAccountsAccessPoliciesView } from "../../models/view/access-policy.view";
|
||||||
import {
|
import {
|
||||||
ProjectAccessPoliciesView,
|
ApItemValueType,
|
||||||
ServiceAccountProjectAccessPolicyView,
|
convertToProjectServiceAccountsAccessPoliciesView,
|
||||||
} from "../../models/view/access-policy.view";
|
} from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type";
|
||||||
|
import {
|
||||||
|
ApItemViewType,
|
||||||
|
convertPotentialGranteesToApItemViewType,
|
||||||
|
convertProjectServiceAccountsViewToApItemViews,
|
||||||
|
} from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type";
|
||||||
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
|
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
|
||||||
import {
|
|
||||||
AccessSelectorComponent,
|
|
||||||
AccessSelectorRowView,
|
|
||||||
} from "../../shared/access-policies/access-selector.component";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "sm-project-service-accounts",
|
selector: "sm-project-service-accounts",
|
||||||
templateUrl: "./project-service-accounts.component.html",
|
templateUrl: "./project-service-accounts.component.html",
|
||||||
})
|
})
|
||||||
export class ProjectServiceAccountsComponent implements OnInit, OnDestroy {
|
export class ProjectServiceAccountsComponent implements OnInit, OnDestroy {
|
||||||
|
private currentAccessPolicies: ApItemViewType[];
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private organizationId: string;
|
private organizationId: string;
|
||||||
private projectId: string;
|
private projectId: string;
|
||||||
|
|
||||||
protected rows$: Observable<AccessSelectorRowView[]> =
|
private currentAccessPolicies$ = combineLatest([this.route.params]).pipe(
|
||||||
this.accessPolicyService.projectAccessPolicyChanges$.pipe(
|
switchMap(([params]) =>
|
||||||
startWith(null),
|
this.accessPolicyService
|
||||||
switchMap(() =>
|
.getProjectServiceAccountsAccessPolicies(params.organizationId, params.projectId)
|
||||||
this.accessPolicyService.getProjectAccessPolicies(this.organizationId, this.projectId),
|
.then((policies) => {
|
||||||
),
|
return convertProjectServiceAccountsViewToApItemViews(policies);
|
||||||
map((policies) =>
|
}),
|
||||||
policies.serviceAccountAccessPolicies.map((policy) => ({
|
),
|
||||||
type: "serviceAccount",
|
);
|
||||||
name: policy.serviceAccountName,
|
|
||||||
id: policy.serviceAccountId,
|
|
||||||
accessPolicyId: policy.id,
|
|
||||||
read: policy.read,
|
|
||||||
write: policy.write,
|
|
||||||
icon: AccessSelectorComponent.serviceAccountIcon,
|
|
||||||
static: false,
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
protected async handleUpdateAccessPolicy(policy: AccessSelectorRowView) {
|
private potentialGrantees$ = combineLatest([this.route.params]).pipe(
|
||||||
try {
|
switchMap(([params]) =>
|
||||||
return await this.accessPolicyService.updateAccessPolicy(
|
this.accessPolicyService
|
||||||
AccessSelectorComponent.getBaseAccessPolicyView(policy),
|
.getServiceAccountsPotentialGrantees(params.organizationId)
|
||||||
);
|
.then((grantees) => {
|
||||||
} catch (e) {
|
return convertPotentialGranteesToApItemViewType(grantees);
|
||||||
this.validationService.showError(e);
|
}),
|
||||||
}
|
),
|
||||||
}
|
);
|
||||||
|
|
||||||
protected handleCreateAccessPolicies(selected: SelectItemView[]) {
|
protected formGroup = new FormGroup({
|
||||||
const projectAccessPoliciesView = new ProjectAccessPoliciesView();
|
accessPolicies: new FormControl([] as ApItemValueType[]),
|
||||||
projectAccessPoliciesView.serviceAccountAccessPolicies = selected
|
});
|
||||||
.filter(
|
|
||||||
(selection) => AccessSelectorComponent.getAccessItemType(selection) === "serviceAccount",
|
|
||||||
)
|
|
||||||
.map((filtered) => {
|
|
||||||
const view = new ServiceAccountProjectAccessPolicyView();
|
|
||||||
view.grantedProjectId = this.projectId;
|
|
||||||
view.serviceAccountId = filtered.id;
|
|
||||||
view.read = true;
|
|
||||||
view.write = false;
|
|
||||||
return view;
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.accessPolicyService.createProjectAccessPolicies(
|
protected loading = true;
|
||||||
this.organizationId,
|
protected potentialGrantees: ApItemViewType[];
|
||||||
this.projectId,
|
protected items: ApItemViewType[];
|
||||||
projectAccessPoliciesView,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) {
|
|
||||||
try {
|
|
||||||
await this.accessPolicyService.deleteAccessPolicy(policy.accessPolicyId);
|
|
||||||
} catch (e) {
|
|
||||||
this.validationService.showError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private validationService: ValidationService,
|
private validationService: ValidationService,
|
||||||
private accessPolicyService: AccessPolicyService,
|
private accessPolicyService: AccessPolicyService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -95,10 +71,97 @@ export class ProjectServiceAccountsComponent implements OnInit, OnDestroy {
|
|||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
this.projectId = params.projectId;
|
this.projectId = params.projectId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
combineLatest([this.potentialGrantees$, this.currentAccessPolicies$])
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe(([potentialGrantees, currentAccessPolicies]) => {
|
||||||
|
this.potentialGrantees = potentialGrantees;
|
||||||
|
this.items = this.getItems(potentialGrantees, currentAccessPolicies);
|
||||||
|
this.setSelected(currentAccessPolicies);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
submit = async () => {
|
||||||
|
if (this.isFormInvalid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formValues = this.formGroup.value.accessPolicies;
|
||||||
|
this.formGroup.disable();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const accessPoliciesView = await this.updateProjectServiceAccountsAccessPolicies(
|
||||||
|
this.organizationId,
|
||||||
|
this.projectId,
|
||||||
|
formValues,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedView = convertProjectServiceAccountsViewToApItemViews(accessPoliciesView);
|
||||||
|
this.items = this.getItems(this.potentialGrantees, updatedView);
|
||||||
|
this.setSelected(updatedView);
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("projectAccessUpdated"),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.validationService.showError(e);
|
||||||
|
this.setSelected(this.currentAccessPolicies);
|
||||||
|
}
|
||||||
|
this.formGroup.enable();
|
||||||
|
};
|
||||||
|
|
||||||
|
private setSelected(policiesToSelect: ApItemViewType[]) {
|
||||||
|
this.loading = true;
|
||||||
|
this.currentAccessPolicies = policiesToSelect;
|
||||||
|
if (policiesToSelect != undefined) {
|
||||||
|
// Must detect changes so that AccessSelector @Inputs() are aware of the latest
|
||||||
|
// potentialGrantees, otherwise no selected values will be patched below
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
accessPolicies: policiesToSelect.map((m) => ({
|
||||||
|
type: m.type,
|
||||||
|
id: m.id,
|
||||||
|
permission: m.permission,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isFormInvalid(): boolean {
|
||||||
|
this.formGroup.markAllAsTouched();
|
||||||
|
return this.formGroup.invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateProjectServiceAccountsAccessPolicies(
|
||||||
|
organizationId: string,
|
||||||
|
projectId: string,
|
||||||
|
selectedPolicies: ApItemValueType[],
|
||||||
|
): Promise<ProjectServiceAccountsAccessPoliciesView> {
|
||||||
|
const view = convertToProjectServiceAccountsAccessPoliciesView(projectId, selectedPolicies);
|
||||||
|
return await this.accessPolicyService.putProjectServiceAccountsAccessPolicies(
|
||||||
|
organizationId,
|
||||||
|
projectId,
|
||||||
|
view,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getItems(potentialGrantees: ApItemViewType[], currentAccessPolicies: ApItemViewType[]) {
|
||||||
|
// If the user doesn't have access to the service account, they won't be in the potentialGrantees list.
|
||||||
|
// Add them to the potentialGrantees list if they are selected.
|
||||||
|
const items = [...potentialGrantees];
|
||||||
|
for (const policy of currentAccessPolicies) {
|
||||||
|
const exists = potentialGrantees.some((grantee) => grantee.id === policy.id);
|
||||||
|
if (!exists) {
|
||||||
|
items.push(policy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
<div class="tw-mt-4 tw-w-2/5">
|
<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner">
|
||||||
<p class="tw-mt-6">
|
<div class="tw-w-2/5">
|
||||||
{{ "machineAccountProjectsDescription" | i18n }}
|
<p class="tw-mt-8" *ngIf="!loading">
|
||||||
</p>
|
{{ "machineAccountProjectsDescription" | i18n }}
|
||||||
<sm-access-selector
|
</p>
|
||||||
[rows]="rows$ | async"
|
<sm-access-policy-selector
|
||||||
granteeType="projects"
|
[loading]="loading"
|
||||||
[label]="'projects' | i18n"
|
formControlName="accessPolicies"
|
||||||
[hint]="'newSaSelectAccess' | i18n"
|
[addButtonMode]="true"
|
||||||
[columnTitle]="'projects' | i18n"
|
[items]="potentialGrantees"
|
||||||
[emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n"
|
[label]="'projects' | i18n"
|
||||||
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)"
|
[hint]="'newSaSelectAccess' | i18n"
|
||||||
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)"
|
[columnTitle]="'projects' | i18n"
|
||||||
(onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)"
|
[emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n"
|
||||||
>
|
>
|
||||||
</sm-access-selector>
|
</sm-access-policy-selector>
|
||||||
</div>
|
<button bitButton buttonType="primary" bitFormButton type="submit" class="tw-mt-7">
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<ng-template #spinner>
|
||||||
|
<div class="tw-items-center tw-justify-center tw-pt-64 tw-text-center">
|
||||||
|
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|||||||
@@ -1,90 +1,68 @@
|
|||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { FormControl, FormGroup } from "@angular/forms";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { combineLatestWith, map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs";
|
import { combineLatest, Subject, switchMap, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view";
|
|
||||||
|
|
||||||
import { ServiceAccountProjectAccessPolicyView } from "../../models/view/access-policy.view";
|
import { ServiceAccountGrantedPoliciesView } from "../../models/view/access-policy.view";
|
||||||
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
|
|
||||||
import {
|
import {
|
||||||
AccessSelectorComponent,
|
ApItemValueType,
|
||||||
AccessSelectorRowView,
|
convertToServiceAccountGrantedPoliciesView,
|
||||||
} from "../../shared/access-policies/access-selector.component";
|
} from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type";
|
||||||
|
import {
|
||||||
|
ApItemViewType,
|
||||||
|
convertPotentialGranteesToApItemViewType,
|
||||||
|
convertGrantedPoliciesToAccessPolicyItemViews,
|
||||||
|
} from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type";
|
||||||
|
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "sm-service-account-projects",
|
selector: "sm-service-account-projects",
|
||||||
templateUrl: "./service-account-projects.component.html",
|
templateUrl: "./service-account-projects.component.html",
|
||||||
})
|
})
|
||||||
export class ServiceAccountProjectsComponent implements OnInit, OnDestroy {
|
export class ServiceAccountProjectsComponent implements OnInit, OnDestroy {
|
||||||
|
private currentAccessPolicies: ApItemViewType[];
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private serviceAccountId: string;
|
|
||||||
private organizationId: string;
|
private organizationId: string;
|
||||||
|
private serviceAccountId: string;
|
||||||
|
|
||||||
protected rows$: Observable<AccessSelectorRowView[]> =
|
private currentAccessPolicies$ = combineLatest([this.route.params]).pipe(
|
||||||
this.accessPolicyService.serviceAccountGrantedPolicyChanges$.pipe(
|
switchMap(([params]) =>
|
||||||
startWith(null),
|
this.accessPolicyService
|
||||||
combineLatestWith(this.route.params),
|
.getServiceAccountGrantedPolicies(params.organizationId, params.serviceAccountId)
|
||||||
switchMap(([_, params]) =>
|
.then((policies) => {
|
||||||
this.accessPolicyService.getGrantedPolicies(params.serviceAccountId, params.organizationId),
|
return convertGrantedPoliciesToAccessPolicyItemViews(policies);
|
||||||
),
|
}),
|
||||||
map((policies) => {
|
),
|
||||||
return policies.map((policy) => {
|
);
|
||||||
return {
|
|
||||||
type: "project",
|
|
||||||
name: policy.grantedProjectName,
|
|
||||||
id: policy.grantedProjectId,
|
|
||||||
accessPolicyId: policy.id,
|
|
||||||
read: policy.read,
|
|
||||||
write: policy.write,
|
|
||||||
icon: AccessSelectorComponent.projectIcon,
|
|
||||||
static: false,
|
|
||||||
} as AccessSelectorRowView;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
protected handleCreateAccessPolicies(selected: SelectItemView[]) {
|
private potentialGrantees$ = combineLatest([this.route.params]).pipe(
|
||||||
const serviceAccountProjectAccessPolicyView = selected
|
switchMap(([params]) =>
|
||||||
.filter((selection) => AccessSelectorComponent.getAccessItemType(selection) === "project")
|
this.accessPolicyService
|
||||||
.map((filtered) => {
|
.getProjectsPotentialGrantees(params.organizationId)
|
||||||
const view = new ServiceAccountProjectAccessPolicyView();
|
.then((grantees) => {
|
||||||
view.serviceAccountId = this.serviceAccountId;
|
return convertPotentialGranteesToApItemViewType(grantees);
|
||||||
view.grantedProjectId = filtered.id;
|
}),
|
||||||
view.read = true;
|
),
|
||||||
view.write = false;
|
);
|
||||||
return view;
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.accessPolicyService.createGrantedPolicies(
|
protected formGroup = new FormGroup({
|
||||||
this.organizationId,
|
accessPolicies: new FormControl([] as ApItemValueType[]),
|
||||||
this.serviceAccountId,
|
});
|
||||||
serviceAccountProjectAccessPolicyView,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async handleUpdateAccessPolicy(policy: AccessSelectorRowView) {
|
protected loading = true;
|
||||||
try {
|
protected potentialGrantees: ApItemViewType[];
|
||||||
return await this.accessPolicyService.updateAccessPolicy(
|
|
||||||
AccessSelectorComponent.getBaseAccessPolicyView(policy),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
this.validationService.showError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) {
|
|
||||||
try {
|
|
||||||
await this.accessPolicyService.deleteAccessPolicy(policy.accessPolicyId);
|
|
||||||
} catch (e) {
|
|
||||||
this.validationService.showError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private validationService: ValidationService,
|
private validationService: ValidationService,
|
||||||
private accessPolicyService: AccessPolicyService,
|
private accessPolicyService: AccessPolicyService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -92,10 +70,119 @@ export class ServiceAccountProjectsComponent implements OnInit, OnDestroy {
|
|||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
this.serviceAccountId = params.serviceAccountId;
|
this.serviceAccountId = params.serviceAccountId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
combineLatest([this.potentialGrantees$, this.currentAccessPolicies$])
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe(([potentialGrantees, currentAccessPolicies]) => {
|
||||||
|
this.potentialGrantees = this.getPotentialGrantees(
|
||||||
|
potentialGrantees,
|
||||||
|
currentAccessPolicies,
|
||||||
|
);
|
||||||
|
this.setSelected(currentAccessPolicies);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
submit = async () => {
|
||||||
|
if (this.isFormInvalid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formValues = this.getFormValues();
|
||||||
|
this.formGroup.disable();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const grantedViews = await this.updateServiceAccountGrantedPolicies(
|
||||||
|
this.organizationId,
|
||||||
|
this.serviceAccountId,
|
||||||
|
formValues,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.currentAccessPolicies = convertGrantedPoliciesToAccessPolicyItemViews(grantedViews);
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("serviceAccountAccessUpdated"),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.validationService.showError(e);
|
||||||
|
this.setSelected(this.currentAccessPolicies);
|
||||||
|
}
|
||||||
|
this.formGroup.enable();
|
||||||
|
};
|
||||||
|
|
||||||
|
private setSelected(policiesToSelect: ApItemViewType[]) {
|
||||||
|
this.loading = true;
|
||||||
|
this.currentAccessPolicies = policiesToSelect;
|
||||||
|
if (policiesToSelect != undefined) {
|
||||||
|
// Must detect changes so that AccessSelector @Inputs() are aware of the latest
|
||||||
|
// potentialGrantees, otherwise no selected values will be patched below
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
accessPolicies: policiesToSelect.map((m) => ({
|
||||||
|
type: m.type,
|
||||||
|
id: m.id,
|
||||||
|
permission: m.permission,
|
||||||
|
readOnly: m.readOnly,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isFormInvalid(): boolean {
|
||||||
|
this.formGroup.markAllAsTouched();
|
||||||
|
return this.formGroup.invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateServiceAccountGrantedPolicies(
|
||||||
|
organizationId: string,
|
||||||
|
serviceAccountId: string,
|
||||||
|
selectedPolicies: ApItemValueType[],
|
||||||
|
): Promise<ServiceAccountGrantedPoliciesView> {
|
||||||
|
const grantedViews = convertToServiceAccountGrantedPoliciesView(
|
||||||
|
serviceAccountId,
|
||||||
|
selectedPolicies,
|
||||||
|
);
|
||||||
|
return await this.accessPolicyService.putServiceAccountGrantedPolicies(
|
||||||
|
organizationId,
|
||||||
|
serviceAccountId,
|
||||||
|
grantedViews,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPotentialGrantees(
|
||||||
|
potentialGrantees: ApItemViewType[],
|
||||||
|
currentAccessPolicies: ApItemViewType[],
|
||||||
|
) {
|
||||||
|
// If the user doesn't have access to the project, they won't be in the potentialGrantees list.
|
||||||
|
// Add them to the potentialGrantees list so they can be selected as read-only.
|
||||||
|
for (const policy of currentAccessPolicies) {
|
||||||
|
const exists = potentialGrantees.some((grantee) => grantee.id === policy.id);
|
||||||
|
if (!exists) {
|
||||||
|
potentialGrantees.push(policy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return potentialGrantees;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFormValues(): ApItemValueType[] {
|
||||||
|
// The read-only disabled form values are not included in the formGroup value.
|
||||||
|
// Manually add them to the returned result to ensure they are included in the form submission.
|
||||||
|
let formValues = this.formGroup.value.accessPolicies;
|
||||||
|
formValues = formValues.concat(
|
||||||
|
this.currentAccessPolicies
|
||||||
|
.filter((m) => m.readOnly)
|
||||||
|
.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
type: m.type,
|
||||||
|
permission: m.permission,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
return formValues;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,14 +29,17 @@
|
|||||||
bitRow
|
bitRow
|
||||||
*ngFor="let item of selectionList.selectedItems; let i = index"
|
*ngFor="let item of selectionList.selectedItems; let i = index"
|
||||||
[formGroupName]="i"
|
[formGroupName]="i"
|
||||||
|
[ngClass]="{ 'tw-text-muted': item.readOnly }"
|
||||||
>
|
>
|
||||||
<td bitCell class="tw-w-0 tw-pr-0">
|
<td bitCell class="tw-w-0 tw-pr-0">
|
||||||
<i class="bwi {{ item.icon }} tw-text-muted" aria-hidden="true"></i>
|
<i class="bwi {{ item.icon }}" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
|
<td bitCell class="tw-max-w-sm tw-truncate">
|
||||||
|
{{ item.labelName }}
|
||||||
</td>
|
</td>
|
||||||
<td bitCell class="tw-max-w-sm tw-truncate">{{ item.labelName }}</td>
|
|
||||||
<td bitCell class="tw-mb-auto tw-inline-block tw-w-auto">
|
<td bitCell class="tw-mb-auto tw-inline-block tw-w-auto">
|
||||||
<select
|
<select
|
||||||
*ngIf="!staticPermission; else static"
|
*ngIf="!staticPermission && !item.readOnly; else readOnly"
|
||||||
bitInput
|
bitInput
|
||||||
formControlName="permission"
|
formControlName="permission"
|
||||||
(blur)="handleBlur()"
|
(blur)="handleBlur()"
|
||||||
@@ -45,12 +48,20 @@
|
|||||||
{{ p.labelId | i18n }}
|
{{ p.labelId | i18n }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
<ng-template #readOnly>
|
||||||
|
<ng-container *ngIf="item.readOnly; else static">
|
||||||
|
<div class="tw-overflow-hidden tw-overflow-ellipsis tw-whitespace-nowrap">
|
||||||
|
{{ item.permission | i18n }}
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
<ng-template #static>
|
<ng-template #static>
|
||||||
<span>{{ staticPermission | i18n }}</span>
|
<span>{{ staticPermission | i18n }}</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
<td bitCell class="tw-w-0">
|
<td bitCell class="tw-w-0">
|
||||||
<button
|
<button
|
||||||
|
*ngIf="!item.readOnly"
|
||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-close"
|
bitIconButton="bwi-close"
|
||||||
buttonType="main"
|
buttonType="main"
|
||||||
|
|||||||
@@ -35,6 +35,34 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn
|
|||||||
private notifyOnTouch: () => void;
|
private notifyOnTouch: () => void;
|
||||||
private pauseChangeNotification: boolean;
|
private pauseChangeNotification: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the enabled/disabled state of provided row form group based on the item's readonly state.
|
||||||
|
* If a row is enabled, it also updates the enabled/disabled state of the permission control
|
||||||
|
* based on the item's accessAllItems state and the current value of `permissionMode`.
|
||||||
|
* @param controlRow - The form group for the row to update
|
||||||
|
* @param item - The access item that is represented by the row
|
||||||
|
*/
|
||||||
|
private updateRowControlDisableState = (
|
||||||
|
controlRow: FormGroup<ControlsOf<ApItemValueType>>,
|
||||||
|
item: ApItemViewType,
|
||||||
|
) => {
|
||||||
|
// Disable entire row form group if readOnly
|
||||||
|
if (item.readOnly || this.disabled) {
|
||||||
|
controlRow.disable();
|
||||||
|
} else {
|
||||||
|
controlRow.enable();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the enabled/disabled state of ALL row form groups based on each item's readonly state.
|
||||||
|
*/
|
||||||
|
private updateAllRowControlDisableStates = () => {
|
||||||
|
this.selectionList.forEachControlItem((controlRow, item) => {
|
||||||
|
this.updateRowControlDisableState(controlRow as FormGroup<ControlsOf<ApItemValueType>>, item);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The internal selection list that tracks the value of this form control / component.
|
* The internal selection list that tracks the value of this form control / component.
|
||||||
* It's responsible for keeping items sorted and synced with the rendered form controls
|
* It's responsible for keeping items sorted and synced with the rendered form controls
|
||||||
@@ -59,6 +87,9 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn
|
|||||||
currentUserInGroup: new FormControl(currentUserInGroup),
|
currentUserInGroup: new FormControl(currentUserInGroup),
|
||||||
currentUser: new FormControl(currentUser),
|
currentUser: new FormControl(currentUser),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.updateRowControlDisableState(fg, item);
|
||||||
|
|
||||||
return fg;
|
return fg;
|
||||||
}, this._itemComparator.bind(this));
|
}, this._itemComparator.bind(this));
|
||||||
|
|
||||||
@@ -100,7 +131,13 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn
|
|||||||
|
|
||||||
set items(val: ApItemViewType[]) {
|
set items(val: ApItemViewType[]) {
|
||||||
if (val != null) {
|
if (val != null) {
|
||||||
const selected = this.selectionList.formArray.getRawValue() ?? [];
|
let selected = this.selectionList.formArray.getRawValue() ?? [];
|
||||||
|
selected = selected.concat(
|
||||||
|
val
|
||||||
|
.filter((m) => m.readOnly)
|
||||||
|
.map((m) => ({ id: m.id, type: m.type, permission: m.permission })),
|
||||||
|
);
|
||||||
|
|
||||||
this.selectionList.populateItems(
|
this.selectionList.populateItems(
|
||||||
val.map((m) => {
|
val.map((m) => {
|
||||||
m.icon = m.icon ?? ApItemEnumUtil.itemIcon(m.type);
|
m.icon = m.icon ?? ApItemEnumUtil.itemIcon(m.type);
|
||||||
@@ -137,6 +174,9 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn
|
|||||||
} else {
|
} else {
|
||||||
this.formGroup.enable();
|
this.formGroup.enable();
|
||||||
this.multiSelectFormGroup.enable();
|
this.multiSelectFormGroup.enable();
|
||||||
|
// The enable() above automatically enables all the row controls,
|
||||||
|
// so we need to disable the readonly ones again
|
||||||
|
this.updateAllRowControlDisableStates();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +189,9 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn
|
|||||||
// Always clear the internal selection list on a new value
|
// Always clear the internal selection list on a new value
|
||||||
this.selectionList.deselectAll();
|
this.selectionList.deselectAll();
|
||||||
|
|
||||||
|
// We need to also select any read only items to appear in the table
|
||||||
|
this.selectionList.selectItems(this.items.filter((m) => m.readOnly).map((m) => m.id));
|
||||||
|
|
||||||
// If the new value is null, then we're done
|
// If the new value is null, then we're done
|
||||||
if (selectedItems == null) {
|
if (selectedItems == null) {
|
||||||
this.pauseChangeNotification = false;
|
this.pauseChangeNotification = false;
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ function createApItemViewType(options: Partial<ApItemViewType> = {}) {
|
|||||||
labelName: options?.labelName ?? "test",
|
labelName: options?.labelName ?? "test",
|
||||||
type: options?.type ?? ApItemEnum.User,
|
type: options?.type ?? ApItemEnum.User,
|
||||||
permission: options?.permission ?? ApPermissionEnum.CanRead,
|
permission: options?.permission ?? ApPermissionEnum.CanRead,
|
||||||
|
readOnly: options?.readOnly ?? false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import {
|
|||||||
ServiceAccountPeopleAccessPoliciesView,
|
ServiceAccountPeopleAccessPoliciesView,
|
||||||
UserServiceAccountAccessPolicyView,
|
UserServiceAccountAccessPolicyView,
|
||||||
GroupServiceAccountAccessPolicyView,
|
GroupServiceAccountAccessPolicyView,
|
||||||
|
ServiceAccountGrantedPoliciesView,
|
||||||
|
ServiceAccountProjectPolicyPermissionDetailsView,
|
||||||
|
ServiceAccountProjectAccessPolicyView,
|
||||||
|
ProjectServiceAccountsAccessPoliciesView,
|
||||||
} from "../../../../models/view/access-policy.view";
|
} from "../../../../models/view/access-policy.view";
|
||||||
|
|
||||||
import { ApItemEnum } from "./enums/ap-item.enum";
|
import { ApItemEnum } from "./enums/ap-item.enum";
|
||||||
@@ -76,3 +80,46 @@ export function convertToServiceAccountPeopleAccessPoliciesView(
|
|||||||
});
|
});
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertToServiceAccountGrantedPoliciesView(
|
||||||
|
serviceAccountId: string,
|
||||||
|
selectedPolicyValues: ApItemValueType[],
|
||||||
|
): ServiceAccountGrantedPoliciesView {
|
||||||
|
const view = new ServiceAccountGrantedPoliciesView();
|
||||||
|
|
||||||
|
view.grantedProjectPolicies = selectedPolicyValues
|
||||||
|
.filter((x) => x.type == ApItemEnum.Project)
|
||||||
|
.map((filtered) => {
|
||||||
|
const detailView = new ServiceAccountProjectPolicyPermissionDetailsView();
|
||||||
|
const policyView = new ServiceAccountProjectAccessPolicyView();
|
||||||
|
policyView.serviceAccountId = serviceAccountId;
|
||||||
|
policyView.grantedProjectId = filtered.id;
|
||||||
|
policyView.read = ApPermissionEnumUtil.toRead(filtered.permission);
|
||||||
|
policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission);
|
||||||
|
|
||||||
|
detailView.accessPolicy = policyView;
|
||||||
|
return detailView;
|
||||||
|
});
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertToProjectServiceAccountsAccessPoliciesView(
|
||||||
|
projectId: string,
|
||||||
|
selectedPolicyValues: ApItemValueType[],
|
||||||
|
): ProjectServiceAccountsAccessPoliciesView {
|
||||||
|
const view = new ProjectServiceAccountsAccessPoliciesView();
|
||||||
|
|
||||||
|
view.serviceAccountAccessPolicies = selectedPolicyValues
|
||||||
|
.filter((x) => x.type == ApItemEnum.ServiceAccount)
|
||||||
|
.map((filtered) => {
|
||||||
|
const policyView = new ServiceAccountProjectAccessPolicyView();
|
||||||
|
policyView.serviceAccountId = filtered.id;
|
||||||
|
policyView.grantedProjectId = projectId;
|
||||||
|
policyView.read = ApPermissionEnumUtil.toRead(filtered.permission);
|
||||||
|
policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission);
|
||||||
|
return policyView;
|
||||||
|
});
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { SelectItemView } from "@bitwarden/components";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ProjectPeopleAccessPoliciesView,
|
ProjectPeopleAccessPoliciesView,
|
||||||
|
ServiceAccountGrantedPoliciesView,
|
||||||
|
ProjectServiceAccountsAccessPoliciesView,
|
||||||
ServiceAccountPeopleAccessPoliciesView,
|
ServiceAccountPeopleAccessPoliciesView,
|
||||||
} from "../../../../models/view/access-policy.view";
|
} from "../../../../models/view/access-policy.view";
|
||||||
import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view";
|
import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view";
|
||||||
@@ -13,6 +15,12 @@ import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.en
|
|||||||
export type ApItemViewType = SelectItemView & {
|
export type ApItemViewType = SelectItemView & {
|
||||||
accessPolicyId?: string;
|
accessPolicyId?: string;
|
||||||
permission?: ApPermissionEnum;
|
permission?: ApPermissionEnum;
|
||||||
|
/**
|
||||||
|
* Flag that this item cannot be modified.
|
||||||
|
* This will disable the permission editor and will keep
|
||||||
|
* the item always selected.
|
||||||
|
*/
|
||||||
|
readOnly: boolean;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
type: ApItemEnum.User;
|
type: ApItemEnum.User;
|
||||||
@@ -47,6 +55,7 @@ export function convertToAccessPolicyItemViews(
|
|||||||
permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write),
|
permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write),
|
||||||
userId: policy.userId,
|
userId: policy.userId,
|
||||||
currentUser: policy.currentUser,
|
currentUser: policy.currentUser,
|
||||||
|
readOnly: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,12 +69,59 @@ export function convertToAccessPolicyItemViews(
|
|||||||
listName: policy.groupName,
|
listName: policy.groupName,
|
||||||
permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write),
|
permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write),
|
||||||
currentUserInGroup: policy.currentUserInGroup,
|
currentUserInGroup: policy.currentUserInGroup,
|
||||||
|
readOnly: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return accessPolicies;
|
return accessPolicies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertGrantedPoliciesToAccessPolicyItemViews(
|
||||||
|
value: ServiceAccountGrantedPoliciesView,
|
||||||
|
): ApItemViewType[] {
|
||||||
|
const accessPolicies: ApItemViewType[] = [];
|
||||||
|
|
||||||
|
value.grantedProjectPolicies.forEach((detailView) => {
|
||||||
|
accessPolicies.push({
|
||||||
|
type: ApItemEnum.Project,
|
||||||
|
icon: ApItemEnumUtil.itemIcon(ApItemEnum.Project),
|
||||||
|
id: detailView.accessPolicy.grantedProjectId,
|
||||||
|
accessPolicyId: detailView.accessPolicy.id,
|
||||||
|
labelName: detailView.accessPolicy.grantedProjectName,
|
||||||
|
listName: detailView.accessPolicy.grantedProjectName,
|
||||||
|
permission: ApPermissionEnumUtil.toApPermissionEnum(
|
||||||
|
detailView.accessPolicy.read,
|
||||||
|
detailView.accessPolicy.write,
|
||||||
|
),
|
||||||
|
readOnly: !detailView.hasPermission,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return accessPolicies;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertProjectServiceAccountsViewToApItemViews(
|
||||||
|
value: ProjectServiceAccountsAccessPoliciesView,
|
||||||
|
): ApItemViewType[] {
|
||||||
|
const accessPolicies: ApItemViewType[] = [];
|
||||||
|
|
||||||
|
value.serviceAccountAccessPolicies.forEach((accessPolicyView) => {
|
||||||
|
accessPolicies.push({
|
||||||
|
type: ApItemEnum.ServiceAccount,
|
||||||
|
icon: ApItemEnumUtil.itemIcon(ApItemEnum.ServiceAccount),
|
||||||
|
id: accessPolicyView.serviceAccountId,
|
||||||
|
accessPolicyId: accessPolicyView.id,
|
||||||
|
labelName: accessPolicyView.serviceAccountName,
|
||||||
|
listName: accessPolicyView.serviceAccountName,
|
||||||
|
permission: ApPermissionEnumUtil.toApPermissionEnum(
|
||||||
|
accessPolicyView.read,
|
||||||
|
accessPolicyView.write,
|
||||||
|
),
|
||||||
|
readOnly: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return accessPolicies;
|
||||||
|
}
|
||||||
|
|
||||||
export function convertPotentialGranteesToApItemViewType(
|
export function convertPotentialGranteesToApItemViewType(
|
||||||
grantees: PotentialGranteeView[],
|
grantees: PotentialGranteeView[],
|
||||||
): ApItemViewType[] {
|
): ApItemViewType[] {
|
||||||
@@ -108,6 +164,7 @@ export function convertPotentialGranteesToApItemViewType(
|
|||||||
listName: listName,
|
listName: listName,
|
||||||
currentUserInGroup: granteeView.currentUserInGroup,
|
currentUserInGroup: granteeView.currentUserInGroup,
|
||||||
currentUser: granteeView.currentUser,
|
currentUser: granteeView.currentUser,
|
||||||
|
readOnly: false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,19 @@ import {
|
|||||||
UserProjectAccessPolicyView,
|
UserProjectAccessPolicyView,
|
||||||
UserServiceAccountAccessPolicyView,
|
UserServiceAccountAccessPolicyView,
|
||||||
ServiceAccountPeopleAccessPoliciesView,
|
ServiceAccountPeopleAccessPoliciesView,
|
||||||
|
ServiceAccountGrantedPoliciesView,
|
||||||
|
ProjectServiceAccountsAccessPoliciesView,
|
||||||
|
ServiceAccountProjectPolicyPermissionDetailsView,
|
||||||
} from "../../models/view/access-policy.view";
|
} from "../../models/view/access-policy.view";
|
||||||
import { PotentialGranteeView } from "../../models/view/potential-grantee.view";
|
import { PotentialGranteeView } from "../../models/view/potential-grantee.view";
|
||||||
import { AccessPoliciesCreateRequest } from "../../shared/access-policies/models/requests/access-policies-create.request";
|
import { AccessPoliciesCreateRequest } from "../../shared/access-policies/models/requests/access-policies-create.request";
|
||||||
import { PeopleAccessPoliciesRequest } from "../../shared/access-policies/models/requests/people-access-policies.request";
|
import { PeopleAccessPoliciesRequest } from "../../shared/access-policies/models/requests/people-access-policies.request";
|
||||||
import { ProjectAccessPoliciesResponse } from "../../shared/access-policies/models/responses/project-access-policies.response";
|
import { ProjectAccessPoliciesResponse } from "../../shared/access-policies/models/responses/project-access-policies.response";
|
||||||
|
import { ServiceAccountGrantedPoliciesRequest } from "../access-policies/models/requests/service-account-granted-policies.request";
|
||||||
|
|
||||||
import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request";
|
import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request";
|
||||||
import { AccessPolicyRequest } from "./models/requests/access-policy.request";
|
import { AccessPolicyRequest } from "./models/requests/access-policy.request";
|
||||||
import { GrantedPolicyRequest } from "./models/requests/granted-policy.request";
|
import { ProjectServiceAccountsAccessPoliciesRequest } from "./models/requests/project-service-accounts-access-policies.request";
|
||||||
import {
|
import {
|
||||||
GroupServiceAccountAccessPolicyResponse,
|
GroupServiceAccountAccessPolicyResponse,
|
||||||
UserServiceAccountAccessPolicyResponse,
|
UserServiceAccountAccessPolicyResponse,
|
||||||
@@ -36,28 +40,22 @@ import {
|
|||||||
} from "./models/responses/access-policy.response";
|
} from "./models/responses/access-policy.response";
|
||||||
import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response";
|
import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response";
|
||||||
import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response";
|
import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response";
|
||||||
|
import { ProjectServiceAccountsAccessPoliciesResponse } from "./models/responses/project-service-accounts-access-policies.response";
|
||||||
|
import { ServiceAccountGrantedPoliciesPermissionDetailsResponse } from "./models/responses/service-account-granted-policies-permission-details.response";
|
||||||
import { ServiceAccountPeopleAccessPoliciesResponse } from "./models/responses/service-account-people-access-policies.response";
|
import { ServiceAccountPeopleAccessPoliciesResponse } from "./models/responses/service-account-people-access-policies.response";
|
||||||
|
import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./models/responses/service-account-project-policy-permission-details.response";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: "root",
|
providedIn: "root",
|
||||||
})
|
})
|
||||||
export class AccessPolicyService {
|
export class AccessPolicyService {
|
||||||
private _projectAccessPolicyChanges$ = new Subject<ProjectAccessPoliciesView>();
|
private _projectAccessPolicyChanges$ = new Subject<ProjectAccessPoliciesView>();
|
||||||
private _serviceAccountGrantedPolicyChanges$ = new Subject<
|
|
||||||
ServiceAccountProjectAccessPolicyView[]
|
|
||||||
>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits when a project access policy is created or deleted.
|
* Emits when a project access policy is created or deleted.
|
||||||
*/
|
*/
|
||||||
readonly projectAccessPolicyChanges$ = this._projectAccessPolicyChanges$.asObservable();
|
readonly projectAccessPolicyChanges$ = this._projectAccessPolicyChanges$.asObservable();
|
||||||
|
|
||||||
/**
|
|
||||||
* Emits when a service account granted policy is created or deleted.
|
|
||||||
*/
|
|
||||||
readonly serviceAccountGrantedPolicyChanges$ =
|
|
||||||
this._serviceAccountGrantedPolicyChanges$.asObservable();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
protected apiService: ApiService,
|
protected apiService: ApiService,
|
||||||
@@ -68,44 +66,6 @@ export class AccessPolicyService {
|
|||||||
this._projectAccessPolicyChanges$.next(null);
|
this._projectAccessPolicyChanges$.next(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGrantedPolicies(
|
|
||||||
serviceAccountId: string,
|
|
||||||
organizationId: string,
|
|
||||||
): Promise<ServiceAccountProjectAccessPolicyView[]> {
|
|
||||||
const r = await this.apiService.send(
|
|
||||||
"GET",
|
|
||||||
"/service-accounts/" + serviceAccountId + "/granted-policies",
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
const results = new ListResponse(r, ServiceAccountProjectAccessPolicyResponse);
|
|
||||||
return await this.createServiceAccountProjectAccessPolicyViews(results.data, organizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createGrantedPolicies(
|
|
||||||
organizationId: string,
|
|
||||||
serviceAccountId: string,
|
|
||||||
policies: ServiceAccountProjectAccessPolicyView[],
|
|
||||||
): Promise<ServiceAccountProjectAccessPolicyView[]> {
|
|
||||||
const request = this.getGrantedPoliciesCreateRequest(policies);
|
|
||||||
const r = await this.apiService.send(
|
|
||||||
"POST",
|
|
||||||
"/service-accounts/" + serviceAccountId + "/granted-policies",
|
|
||||||
request,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
const results = new ListResponse(r, ServiceAccountProjectAccessPolicyResponse);
|
|
||||||
const views = await this.createServiceAccountProjectAccessPolicyViews(
|
|
||||||
results.data,
|
|
||||||
organizationId,
|
|
||||||
);
|
|
||||||
this._serviceAccountGrantedPolicyChanges$.next(views);
|
|
||||||
return views;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProjectAccessPolicies(
|
async getProjectAccessPolicies(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@@ -184,6 +144,74 @@ export class AccessPolicyService {
|
|||||||
return this.createServiceAccountPeopleAccessPoliciesView(results);
|
return this.createServiceAccountPeopleAccessPoliciesView(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getServiceAccountGrantedPolicies(
|
||||||
|
organizationId: string,
|
||||||
|
serviceAccountId: string,
|
||||||
|
): Promise<ServiceAccountGrantedPoliciesView> {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
"/service-accounts/" + serviceAccountId + "/granted-policies",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = new ServiceAccountGrantedPoliciesPermissionDetailsResponse(r);
|
||||||
|
return await this.createServiceAccountGrantedPoliciesView(result, organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async putServiceAccountGrantedPolicies(
|
||||||
|
organizationId: string,
|
||||||
|
serviceAccountId: string,
|
||||||
|
policies: ServiceAccountGrantedPoliciesView,
|
||||||
|
): Promise<ServiceAccountGrantedPoliciesView> {
|
||||||
|
const request = this.getServiceAccountGrantedPoliciesRequest(policies);
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"PUT",
|
||||||
|
"/service-accounts/" + serviceAccountId + "/granted-policies",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = new ServiceAccountGrantedPoliciesPermissionDetailsResponse(r);
|
||||||
|
return await this.createServiceAccountGrantedPoliciesView(result, organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProjectServiceAccountsAccessPolicies(
|
||||||
|
organizationId: string,
|
||||||
|
projectId: string,
|
||||||
|
): Promise<ProjectServiceAccountsAccessPoliciesView> {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
"/projects/" + projectId + "/access-policies/service-accounts",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = new ProjectServiceAccountsAccessPoliciesResponse(r);
|
||||||
|
return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async putProjectServiceAccountsAccessPolicies(
|
||||||
|
organizationId: string,
|
||||||
|
projectId: string,
|
||||||
|
policies: ProjectServiceAccountsAccessPoliciesView,
|
||||||
|
): Promise<ProjectServiceAccountsAccessPoliciesView> {
|
||||||
|
const request = this.getProjectServiceAccountsAccessPoliciesRequest(policies);
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"PUT",
|
||||||
|
"/projects/" + projectId + "/access-policies/service-accounts",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = new ProjectServiceAccountsAccessPoliciesResponse(r);
|
||||||
|
return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
async createProjectAccessPolicies(
|
async createProjectAccessPolicies(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@@ -206,7 +234,6 @@ export class AccessPolicyService {
|
|||||||
async deleteAccessPolicy(accessPolicyId: string): Promise<void> {
|
async deleteAccessPolicy(accessPolicyId: string): Promise<void> {
|
||||||
await this.apiService.send("DELETE", "/access-policies/" + accessPolicyId, null, true, false);
|
await this.apiService.send("DELETE", "/access-policies/" + accessPolicyId, null, true, false);
|
||||||
this._projectAccessPolicyChanges$.next(null);
|
this._projectAccessPolicyChanges$.next(null);
|
||||||
this._serviceAccountGrantedPolicyChanges$.next(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAccessPolicy(baseAccessPolicyView: BaseAccessPolicyView): Promise<void> {
|
async updateAccessPolicy(baseAccessPolicyView: BaseAccessPolicyView): Promise<void> {
|
||||||
@@ -222,6 +249,170 @@ export class AccessPolicyService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPeoplePotentialGrantees(organizationId: string) {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
"/organizations/" + organizationId + "/access-policies/people/potential-grantees",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const results = new ListResponse(r, PotentialGranteeResponse);
|
||||||
|
return await this.createPotentialGranteeViews(organizationId, results.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getServiceAccountsPotentialGrantees(organizationId: string) {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
"/organizations/" + organizationId + "/access-policies/service-accounts/potential-grantees",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const results = new ListResponse(r, PotentialGranteeResponse);
|
||||||
|
return await this.createPotentialGranteeViews(organizationId, results.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProjectsPotentialGrantees(organizationId: string) {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
"/organizations/" + organizationId + "/access-policies/projects/potential-grantees",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const results = new ListResponse(r, PotentialGranteeResponse);
|
||||||
|
return await this.createPotentialGranteeViews(organizationId, results.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
||||||
|
return await this.cryptoService.getOrgKey(organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getAccessPolicyRequest(
|
||||||
|
granteeId: string,
|
||||||
|
view:
|
||||||
|
| UserProjectAccessPolicyView
|
||||||
|
| UserServiceAccountAccessPolicyView
|
||||||
|
| GroupProjectAccessPolicyView
|
||||||
|
| GroupServiceAccountAccessPolicyView
|
||||||
|
| ServiceAccountProjectAccessPolicyView,
|
||||||
|
) {
|
||||||
|
const request = new AccessPolicyRequest();
|
||||||
|
request.granteeId = granteeId;
|
||||||
|
request.read = view.read;
|
||||||
|
request.write = view.write;
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createBaseAccessPolicyView(
|
||||||
|
response:
|
||||||
|
| UserProjectAccessPolicyResponse
|
||||||
|
| UserServiceAccountAccessPolicyResponse
|
||||||
|
| GroupProjectAccessPolicyResponse
|
||||||
|
| GroupServiceAccountAccessPolicyResponse
|
||||||
|
| ServiceAccountProjectAccessPolicyResponse,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
id: response.id,
|
||||||
|
read: response.read,
|
||||||
|
write: response.write,
|
||||||
|
creationDate: response.creationDate,
|
||||||
|
revisionDate: response.revisionDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createPotentialGranteeViews(
|
||||||
|
organizationId: string,
|
||||||
|
results: PotentialGranteeResponse[],
|
||||||
|
): Promise<PotentialGranteeView[]> {
|
||||||
|
const orgKey = await this.getOrganizationKey(organizationId);
|
||||||
|
return await Promise.all(
|
||||||
|
results.map(async (r) => {
|
||||||
|
const view = new PotentialGranteeView();
|
||||||
|
view.id = r.id;
|
||||||
|
view.type = r.type;
|
||||||
|
view.email = r.email;
|
||||||
|
view.currentUser = r.currentUser;
|
||||||
|
view.currentUserInGroup = r.currentUserInGroup;
|
||||||
|
|
||||||
|
if (r.type === "serviceAccount" || r.type === "project") {
|
||||||
|
view.name = r.name
|
||||||
|
? await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey)
|
||||||
|
: null;
|
||||||
|
} else {
|
||||||
|
view.name = r.name;
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getServiceAccountGrantedPoliciesRequest(
|
||||||
|
policies: ServiceAccountGrantedPoliciesView,
|
||||||
|
): ServiceAccountGrantedPoliciesRequest {
|
||||||
|
const request = new ServiceAccountGrantedPoliciesRequest();
|
||||||
|
|
||||||
|
request.projectGrantedPolicyRequests = policies.grantedProjectPolicies.map((detailView) => ({
|
||||||
|
grantedId: detailView.accessPolicy.grantedProjectId,
|
||||||
|
read: detailView.accessPolicy.read,
|
||||||
|
write: detailView.accessPolicy.write,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getProjectServiceAccountsAccessPoliciesRequest(
|
||||||
|
policies: ProjectServiceAccountsAccessPoliciesView,
|
||||||
|
): ProjectServiceAccountsAccessPoliciesRequest {
|
||||||
|
const request = new ProjectServiceAccountsAccessPoliciesRequest();
|
||||||
|
|
||||||
|
request.serviceAccountAccessPolicyRequests = policies.serviceAccountAccessPolicies.map((ap) => {
|
||||||
|
return this.getAccessPolicyRequest(ap.serviceAccountId, ap);
|
||||||
|
});
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createServiceAccountGrantedPoliciesView(
|
||||||
|
response: ServiceAccountGrantedPoliciesPermissionDetailsResponse,
|
||||||
|
organizationId: string,
|
||||||
|
): Promise<ServiceAccountGrantedPoliciesView> {
|
||||||
|
const orgKey = await this.getOrganizationKey(organizationId);
|
||||||
|
|
||||||
|
const view = new ServiceAccountGrantedPoliciesView();
|
||||||
|
view.grantedProjectPolicies =
|
||||||
|
await this.createServiceAccountProjectPolicyPermissionDetailsViews(
|
||||||
|
orgKey,
|
||||||
|
response.grantedProjectPolicies,
|
||||||
|
);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createServiceAccountProjectPolicyPermissionDetailsViews(
|
||||||
|
orgKey: SymmetricCryptoKey,
|
||||||
|
responses: ServiceAccountProjectPolicyPermissionDetailsResponse[],
|
||||||
|
): Promise<ServiceAccountProjectPolicyPermissionDetailsView[]> {
|
||||||
|
return await Promise.all(
|
||||||
|
responses.map(async (response) => {
|
||||||
|
return await this.createServiceAccountProjectPolicyPermissionDetailsView(orgKey, response);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createServiceAccountProjectPolicyPermissionDetailsView(
|
||||||
|
orgKey: SymmetricCryptoKey,
|
||||||
|
response: ServiceAccountProjectPolicyPermissionDetailsResponse,
|
||||||
|
): Promise<ServiceAccountProjectPolicyPermissionDetailsView> {
|
||||||
|
const view = new ServiceAccountProjectPolicyPermissionDetailsView();
|
||||||
|
view.hasPermission = response.hasPermission;
|
||||||
|
view.accessPolicy = await this.createServiceAccountProjectAccessPolicyView(
|
||||||
|
orgKey,
|
||||||
|
response.accessPolicy,
|
||||||
|
);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
private async createProjectAccessPoliciesView(
|
private async createProjectAccessPoliciesView(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
projectAccessPoliciesResponse: ProjectAccessPoliciesResponse,
|
projectAccessPoliciesResponse: ProjectAccessPoliciesResponse,
|
||||||
@@ -394,146 +585,18 @@ export class AccessPolicyService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPeoplePotentialGrantees(organizationId: string) {
|
private async createProjectServiceAccountsAccessPoliciesView(
|
||||||
const r = await this.apiService.send(
|
response: ProjectServiceAccountsAccessPoliciesResponse,
|
||||||
"GET",
|
|
||||||
"/organizations/" + organizationId + "/access-policies/people/potential-grantees",
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
const results = new ListResponse(r, PotentialGranteeResponse);
|
|
||||||
return await this.createPotentialGranteeViews(organizationId, results.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getServiceAccountsPotentialGrantees(organizationId: string) {
|
|
||||||
const r = await this.apiService.send(
|
|
||||||
"GET",
|
|
||||||
"/organizations/" + organizationId + "/access-policies/service-accounts/potential-grantees",
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
const results = new ListResponse(r, PotentialGranteeResponse);
|
|
||||||
return await this.createPotentialGranteeViews(organizationId, results.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProjectsPotentialGrantees(organizationId: string) {
|
|
||||||
const r = await this.apiService.send(
|
|
||||||
"GET",
|
|
||||||
"/organizations/" + organizationId + "/access-policies/projects/potential-grantees",
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
const results = new ListResponse(r, PotentialGranteeResponse);
|
|
||||||
return await this.createPotentialGranteeViews(organizationId, results.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
|
||||||
return await this.cryptoService.getOrgKey(organizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getAccessPolicyRequest(
|
|
||||||
granteeId: string,
|
|
||||||
view:
|
|
||||||
| UserProjectAccessPolicyView
|
|
||||||
| UserServiceAccountAccessPolicyView
|
|
||||||
| GroupProjectAccessPolicyView
|
|
||||||
| GroupServiceAccountAccessPolicyView
|
|
||||||
| ServiceAccountProjectAccessPolicyView,
|
|
||||||
) {
|
|
||||||
const request = new AccessPolicyRequest();
|
|
||||||
request.granteeId = granteeId;
|
|
||||||
request.read = view.read;
|
|
||||||
request.write = view.write;
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected createBaseAccessPolicyView(
|
|
||||||
response:
|
|
||||||
| UserProjectAccessPolicyResponse
|
|
||||||
| UserServiceAccountAccessPolicyResponse
|
|
||||||
| GroupProjectAccessPolicyResponse
|
|
||||||
| GroupServiceAccountAccessPolicyResponse
|
|
||||||
| ServiceAccountProjectAccessPolicyResponse,
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
id: response.id,
|
|
||||||
read: response.read,
|
|
||||||
write: response.write,
|
|
||||||
creationDate: response.creationDate,
|
|
||||||
revisionDate: response.revisionDate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createPotentialGranteeViews(
|
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
results: PotentialGranteeResponse[],
|
): Promise<ProjectServiceAccountsAccessPoliciesView> {
|
||||||
): Promise<PotentialGranteeView[]> {
|
|
||||||
const orgKey = await this.getOrganizationKey(organizationId);
|
const orgKey = await this.getOrganizationKey(organizationId);
|
||||||
return await Promise.all(
|
|
||||||
results.map(async (r) => {
|
|
||||||
const view = new PotentialGranteeView();
|
|
||||||
view.id = r.id;
|
|
||||||
view.type = r.type;
|
|
||||||
view.email = r.email;
|
|
||||||
view.currentUser = r.currentUser;
|
|
||||||
view.currentUserInGroup = r.currentUserInGroup;
|
|
||||||
|
|
||||||
if (r.type === "serviceAccount" || r.type === "project") {
|
const view = new ProjectServiceAccountsAccessPoliciesView();
|
||||||
view.name = r.name
|
view.serviceAccountAccessPolicies = await Promise.all(
|
||||||
? await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey)
|
response.serviceAccountAccessPolicies.map(async (ap) => {
|
||||||
: null;
|
return await this.createServiceAccountProjectAccessPolicyView(orgKey, ap);
|
||||||
} else {
|
|
||||||
view.name = r.name;
|
|
||||||
}
|
|
||||||
return view;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getGrantedPoliciesCreateRequest(
|
|
||||||
policies: ServiceAccountProjectAccessPolicyView[],
|
|
||||||
): GrantedPolicyRequest[] {
|
|
||||||
return policies.map((ap) => {
|
|
||||||
const request = new GrantedPolicyRequest();
|
|
||||||
request.grantedId = ap.grantedProjectId;
|
|
||||||
request.read = ap.read;
|
|
||||||
request.write = ap.write;
|
|
||||||
return request;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createServiceAccountProjectAccessPolicyViews(
|
|
||||||
responses: ServiceAccountProjectAccessPolicyResponse[],
|
|
||||||
organizationId: string,
|
|
||||||
): Promise<ServiceAccountProjectAccessPolicyView[]> {
|
|
||||||
const orgKey = await this.getOrganizationKey(organizationId);
|
|
||||||
return await Promise.all(
|
|
||||||
responses.map(async (response: ServiceAccountProjectAccessPolicyResponse) => {
|
|
||||||
const view = new ServiceAccountProjectAccessPolicyView();
|
|
||||||
view.id = response.id;
|
|
||||||
view.read = response.read;
|
|
||||||
view.write = response.write;
|
|
||||||
view.creationDate = response.creationDate;
|
|
||||||
view.revisionDate = response.revisionDate;
|
|
||||||
view.serviceAccountId = response.serviceAccountId;
|
|
||||||
view.grantedProjectId = response.grantedProjectId;
|
|
||||||
view.serviceAccountName = response.serviceAccountName
|
|
||||||
? await this.encryptService.decryptToUtf8(
|
|
||||||
new EncString(response.serviceAccountName),
|
|
||||||
orgKey,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
view.grantedProjectName = response.grantedProjectName
|
|
||||||
? await this.encryptService.decryptToUtf8(
|
|
||||||
new EncString(response.grantedProjectName),
|
|
||||||
orgKey,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
return view;
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
return view;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { AccessPolicyRequest } from "./access-policy.request";
|
||||||
|
|
||||||
|
export class ProjectServiceAccountsAccessPoliciesRequest {
|
||||||
|
serviceAccountAccessPolicyRequests?: AccessPolicyRequest[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { GrantedPolicyRequest } from "./granted-policy.request";
|
||||||
|
|
||||||
|
export class ServiceAccountGrantedPoliciesRequest {
|
||||||
|
projectGrantedPolicyRequests?: GrantedPolicyRequest[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||||
|
|
||||||
|
import { ServiceAccountProjectAccessPolicyResponse } from "./access-policy.response";
|
||||||
|
|
||||||
|
export class ProjectServiceAccountsAccessPoliciesResponse extends BaseResponse {
|
||||||
|
serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyResponse[];
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
const serviceAccountAccessPolicies = this.getResponseProperty("ServiceAccountAccessPolicies");
|
||||||
|
this.serviceAccountAccessPolicies = serviceAccountAccessPolicies.map(
|
||||||
|
(k: any) => new ServiceAccountProjectAccessPolicyResponse(k),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||||
|
|
||||||
|
import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./service-account-project-policy-permission-details.response";
|
||||||
|
|
||||||
|
export class ServiceAccountGrantedPoliciesPermissionDetailsResponse extends BaseResponse {
|
||||||
|
grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsResponse[];
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
const grantedProjectPolicies = this.getResponseProperty("GrantedProjectPolicies");
|
||||||
|
this.grantedProjectPolicies = grantedProjectPolicies.map(
|
||||||
|
(k: any) => new ServiceAccountProjectPolicyPermissionDetailsResponse(k),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||||
|
|
||||||
|
import { ServiceAccountProjectAccessPolicyResponse } from "./access-policy.response";
|
||||||
|
|
||||||
|
export class ServiceAccountProjectPolicyPermissionDetailsResponse extends BaseResponse {
|
||||||
|
accessPolicy: ServiceAccountProjectAccessPolicyResponse;
|
||||||
|
hasPermission: boolean;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.accessPolicy = this.getResponseProperty("AccessPolicy");
|
||||||
|
this.hasPermission = this.getResponseProperty("HasPermission");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -656,7 +656,6 @@ const safeProviders: SafeProvider[] = [
|
|||||||
CipherServiceAbstraction,
|
CipherServiceAbstraction,
|
||||||
FolderServiceAbstraction,
|
FolderServiceAbstraction,
|
||||||
CollectionServiceAbstraction,
|
CollectionServiceAbstraction,
|
||||||
CryptoServiceAbstraction,
|
|
||||||
PlatformUtilsServiceAbstraction,
|
PlatformUtilsServiceAbstraction,
|
||||||
MessagingServiceAbstraction,
|
MessagingServiceAbstraction,
|
||||||
SearchServiceAbstraction,
|
SearchServiceAbstraction,
|
||||||
|
|||||||
@@ -289,6 +289,16 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Only Admins can clone a cipher to different owner
|
||||||
|
if (this.cloneMode && this.cipher.organizationId != null) {
|
||||||
|
const cipherOrg = (await firstValueFrom(this.organizationService.memberOrganizations$)).find(
|
||||||
|
(o) => o.id === this.cipher.organizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cipherOrg != null && !cipherOrg.isAdmin && !cipherOrg.permissions.editAnyCollection) {
|
||||||
|
this.ownershipOptions = [{ name: cipherOrg.name, value: cipherOrg.id }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We don't want to copy passkeys when we clone a cipher
|
// We don't want to copy passkeys when we clone a cipher
|
||||||
if (this.cloneMode && this.cipher?.login?.hasFido2Credentials) {
|
if (this.cloneMode && this.cipher?.login?.hasFido2Credentials) {
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ export abstract class CryptoService {
|
|||||||
kdfConfig: KdfConfig,
|
kdfConfig: KdfConfig,
|
||||||
oldPinKey: EncString,
|
oldPinKey: EncString,
|
||||||
): Promise<UserKey>;
|
): Promise<UserKey>;
|
||||||
/**
|
|
||||||
* Replaces old master auto keys with new user auto keys
|
|
||||||
*/
|
|
||||||
abstract migrateAutoKeyIfNeeded(userId?: string): Promise<void>;
|
|
||||||
/**
|
/**
|
||||||
* @param keyMaterial The key material to derive the send key from
|
* @param keyMaterial The key material to derive the send key from
|
||||||
* @returns A new send key
|
* @returns A new send key
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user