Merge branch 'main' of https://github.com/bitwarden/clients into ac/ac-217/migrate-to-cl-banners
This commit is contained in:
commit
7f215207ee
|
@ -1120,9 +1120,6 @@
|
|||
"commandLockVaultDesc": {
|
||||
"message": "Lock the vault"
|
||||
},
|
||||
"privateModeWarning": {
|
||||
"message": "Private mode support is experimental and some features are limited."
|
||||
},
|
||||
"customFields": {
|
||||
"message": "Custom fields"
|
||||
},
|
||||
|
|
|
@ -89,7 +89,6 @@
|
|||
<p class="text-center" *ngIf="!fido2Data.isFido2Session">
|
||||
<button type="button" appStopClick (click)="logOut()">{{ "logOut" | i18n }}</button>
|
||||
</p>
|
||||
<app-private-mode-warning></app-private-mode-warning>
|
||||
<app-callout *ngIf="biometricError" type="error">{{ biometricError }}</app-callout>
|
||||
<p class="text-center text-muted" *ngIf="pendingBiometric">
|
||||
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i> {{ "awaitDesktop" | i18n }}
|
||||
|
|
|
@ -57,7 +57,6 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-private-mode-warning></app-private-mode-warning>
|
||||
<div class="content login-buttons">
|
||||
<button type="submit" class="btn primary block" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading"
|
||||
|
|
|
@ -109,7 +109,6 @@ import { EncryptServiceImplementation } from "@bitwarden/common/platform/service
|
|||
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 { 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 { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||
|
@ -356,10 +355,7 @@ export default class MainBackground {
|
|||
private isSafari: boolean;
|
||||
private nativeMessagingBackground: NativeMessagingBackground;
|
||||
|
||||
constructor(
|
||||
public isPrivateMode: boolean = false,
|
||||
public popupOnlyContext: boolean = false,
|
||||
) {
|
||||
constructor(public popupOnlyContext: boolean = false) {
|
||||
// Services
|
||||
const lockedCallback = async (userId?: string) => {
|
||||
if (this.notificationsService != null) {
|
||||
|
@ -443,10 +439,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.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3)
|
||||
? 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.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)
|
||||
? mv3MemoryStorageCreator() // mv3 stores to local-backed session storage
|
||||
: this.memoryStorageForStateProviders; // mv2 stores to the same location
|
||||
|
@ -1109,27 +1109,9 @@ export default class MainBackground {
|
|||
await this.idleBackground.init();
|
||||
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) => {
|
||||
setTimeout(async () => {
|
||||
if (!this.isPrivateMode) {
|
||||
await this.refreshBadge();
|
||||
}
|
||||
await this.refreshBadge();
|
||||
await this.fullSync(true);
|
||||
setTimeout(() => this.notificationsService.init(), 2500);
|
||||
resolve();
|
||||
|
|
|
@ -204,10 +204,6 @@ export class BrowserApi {
|
|||
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) {
|
||||
// FIXME: Make sure that is does not cause a memory leak in Safari or use BrowserApi.AddListener
|
||||
// and test that it doesn't break.
|
||||
|
|
|
@ -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", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(BrowserApi, "getWindow").mockResolvedValueOnce({
|
||||
|
|
|
@ -89,13 +89,6 @@ class BrowserPopupUtils {
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -62,15 +62,6 @@ export class DefaultBrowserStateService
|
|||
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
|
||||
// to delete the cache in the constructor above.
|
||||
protected override async saveAccountToDisk(
|
||||
|
|
|
@ -70,7 +70,6 @@ import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.
|
|||
import { AppRoutingModule } from "./app-routing.module";
|
||||
import { AppComponent } from "./app.component";
|
||||
import { PopOutComponent } from "./components/pop-out.component";
|
||||
import { PrivateModeWarningComponent } from "./components/private-mode-warning.component";
|
||||
import { UserVerificationComponent } from "./components/user-verification.component";
|
||||
import { ServicesModule } from "./services/services.module";
|
||||
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
|
||||
|
@ -150,7 +149,6 @@ import "../platform/popup/locales";
|
|||
PasswordHistoryComponent,
|
||||
PopOutComponent,
|
||||
PremiumComponent,
|
||||
PrivateModeWarningComponent,
|
||||
RegisterComponent,
|
||||
SendAddEditComponent,
|
||||
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-xs {
|
||||
app-home {
|
||||
|
|
|
@ -28,7 +28,9 @@ import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/a
|
|||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
|
@ -53,6 +55,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.
|
|||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
@ -61,6 +64,7 @@ import {
|
|||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} 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 { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
||||
|
@ -100,6 +104,7 @@ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
|||
import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service";
|
||||
import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.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 BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
|
||||
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
|
||||
|
@ -125,13 +130,12 @@ const OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE = new SafeInjectionToken<
|
|||
>("OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE");
|
||||
|
||||
const needsBackgroundInit = BrowserPopupUtils.backgroundInitializationRequired();
|
||||
const isPrivateMode = BrowserPopupUtils.inPrivateMode();
|
||||
const mainBackground: MainBackground = needsBackgroundInit
|
||||
? createLocalBgService()
|
||||
: BrowserApi.getBackgroundPage().bitwardenMain;
|
||||
|
||||
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.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
localBgService.bootstrap();
|
||||
|
@ -220,12 +224,48 @@ const safeProviders: SafeProvider[] = [
|
|||
}),
|
||||
safeProvider({
|
||||
provide: CryptoService,
|
||||
useFactory: (encryptService: EncryptService) => {
|
||||
const cryptoService = getBgService<CryptoService>("cryptoService")();
|
||||
useFactory: (
|
||||
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);
|
||||
return cryptoService;
|
||||
},
|
||||
deps: [EncryptService],
|
||||
deps: [
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
CryptoFunctionService,
|
||||
EncryptService,
|
||||
PlatformUtilsService,
|
||||
LogService,
|
||||
StateServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
StateProvider,
|
||||
BiometricStateService,
|
||||
KdfConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TotpServiceAbstraction,
|
||||
|
|
|
@ -146,6 +146,8 @@ export class NativeMessagingMain {
|
|||
allowed_origins: [
|
||||
// Chrome extension
|
||||
"chrome-extension://nngceckbapebfimnlniiiahkandclblb/",
|
||||
// Chrome beta extension
|
||||
"chrome-extension://hccnnhgbibccigepcmlgppchkpfdophk/",
|
||||
// Edge extension
|
||||
"chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/",
|
||||
// Opera extension
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router";
|
|||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
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 { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
@ -27,11 +28,20 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
|
|||
organizationService: OrganizationService,
|
||||
private route: ActivatedRoute,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
i18nService: I18nService,
|
||||
) {
|
||||
super(cipherService, auditService, organizationService, modalService, passwordRepromptService);
|
||||
super(
|
||||
cipherService,
|
||||
auditService,
|
||||
organizationService,
|
||||
modalService,
|
||||
passwordRepromptService,
|
||||
i18nService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.isAdminConsoleActive = true;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
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 { 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
@ -24,11 +25,20 @@ export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorRepor
|
|||
logService: LogService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
organizationService: OrganizationService,
|
||||
i18nService: I18nService,
|
||||
) {
|
||||
super(cipherService, organizationService, modalService, logService, passwordRepromptService);
|
||||
super(
|
||||
cipherService,
|
||||
organizationService,
|
||||
modalService,
|
||||
logService,
|
||||
passwordRepromptService,
|
||||
i18nService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.isAdminConsoleActive = true;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
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 { 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 { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
@ -25,11 +26,13 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom
|
|||
private route: ActivatedRoute,
|
||||
organizationService: OrganizationService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
i18nService: I18nService,
|
||||
) {
|
||||
super(cipherService, organizationService, modalService, passwordRepromptService);
|
||||
super(cipherService, organizationService, modalService, passwordRepromptService, i18nService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.isAdminConsoleActive = true;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
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 { 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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
@ -22,11 +23,13 @@ export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesRepor
|
|||
private route: ActivatedRoute,
|
||||
organizationService: OrganizationService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
i18nService: I18nService,
|
||||
) {
|
||||
super(cipherService, organizationService, modalService, passwordRepromptService);
|
||||
super(cipherService, organizationService, modalService, passwordRepromptService, i18nService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.isAdminConsoleActive = true;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
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 { 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
|
@ -27,6 +28,7 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone
|
|||
private route: ActivatedRoute,
|
||||
organizationService: OrganizationService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
i18nService: I18nService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
|
@ -34,10 +36,12 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone
|
|||
organizationService,
|
||||
modalService,
|
||||
passwordRepromptService,
|
||||
i18nService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.isAdminConsoleActive = true;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.organizationService.get(params.organizationId);
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { Component, ViewChild } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { combineLatest, concatMap } from "rxjs";
|
||||
import { ActivatedRoute, ParamMap, Router } from "@angular/router";
|
||||
import { combineLatest, concatMap, map } from "rxjs";
|
||||
|
||||
import {
|
||||
canAccessOrgAdmin,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
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";
|
||||
|
||||
type ProductSwitcherItem = {
|
||||
|
@ -48,6 +49,13 @@ export class ProductSwitcherContentComponent {
|
|||
this.organizationService.organizations$,
|
||||
this.route.paramMap,
|
||||
]).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]) => {
|
||||
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.
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { Directive, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
import { Directive, ViewChild, ViewContainerRef, OnDestroy } from "@angular/core";
|
||||
import { BehaviorSubject, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
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";
|
||||
|
||||
@Directive()
|
||||
export class CipherReportComponent {
|
||||
export class CipherReportComponent implements OnDestroy {
|
||||
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
|
||||
cipherAddEditModalRef: ViewContainerRef;
|
||||
isAdminConsoleActive = false;
|
||||
|
||||
loading = false;
|
||||
hasLoaded = false;
|
||||
ciphers: CipherView[] = [];
|
||||
allCiphers: CipherView[] = [];
|
||||
organization: Organization;
|
||||
organizations: 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(
|
||||
protected cipherService: CipherService,
|
||||
private modalService: ModalService,
|
||||
protected passwordRepromptService: PasswordRepromptService,
|
||||
protected organizationService: OrganizationService,
|
||||
protected i18nService: I18nService,
|
||||
) {
|
||||
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() {
|
||||
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.hasLoaded = true;
|
||||
}
|
||||
|
@ -76,7 +162,7 @@ export class CipherReportComponent {
|
|||
}
|
||||
|
||||
protected async setCiphers() {
|
||||
this.ciphers = [];
|
||||
this.allCiphers = [];
|
||||
}
|
||||
|
||||
protected async repromptCipher(c: CipherView) {
|
||||
|
@ -85,4 +171,32 @@ export class CipherReportComponent {
|
|||
(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>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{ 'exposedPasswordsFound' | i18n }}" [useAlertRole]="true">
|
||||
{{ "exposedPasswordsFoundDesc" | i18n: (ciphers.length | number) }}
|
||||
{{ "exposedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||
</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">
|
||||
<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>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
|
@ -17,9 +18,12 @@ describe("ExposedPasswordsReportComponent", () => {
|
|||
let component: ExposedPasswordsReportComponent;
|
||||
let fixture: ComponentFixture<ExposedPasswordsReportComponent>;
|
||||
let auditService: MockProxy<AuditService>;
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
|
||||
beforeEach(() => {
|
||||
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.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
TestBed.configureTestingModule({
|
||||
|
@ -35,7 +39,7 @@ describe("ExposedPasswordsReportComponent", () => {
|
|||
},
|
||||
{
|
||||
provide: OrganizationService,
|
||||
useValue: mock<OrganizationService>(),
|
||||
useValue: organizationService,
|
||||
},
|
||||
{
|
||||
provide: ModalService,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core";
|
|||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
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 { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
@ -24,8 +25,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
|
|||
protected organizationService: OrganizationService,
|
||||
modalService: ModalService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
i18nService: I18nService,
|
||||
) {
|
||||
super(modalService, passwordRepromptService, organizationService);
|
||||
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -36,7 +38,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
|
|||
const allCiphers = await this.getAllCiphers();
|
||||
const exposedPasswordCiphers: CipherView[] = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
allCiphers.forEach((ciph) => {
|
||||
this.filterStatus = [0];
|
||||
|
||||
allCiphers.forEach((ciph: any) => {
|
||||
const { type, login, isDeleted, edit, viewPassword, id } = ciph;
|
||||
if (
|
||||
type !== CipherType.Login ||
|
||||
|
@ -48,6 +52,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
|
|||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => {
|
||||
if (exposedCount > 0) {
|
||||
exposedPasswordCiphers.push(ciph);
|
||||
|
@ -57,11 +62,8 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
|
|||
promises.push(promise);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
this.ciphers = [...exposedPasswordCiphers];
|
||||
}
|
||||
|
||||
protected getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllDecrypted();
|
||||
this.filterCiphersByOrg(exposedPasswordCiphers);
|
||||
}
|
||||
|
||||
protected canManageCipher(c: CipherView): boolean {
|
||||
|
|
|
@ -16,9 +16,32 @@
|
|||
</app-callout>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{ 'inactive2faFound' | i18n }}">
|
||||
{{ "inactive2faFoundDesc" | i18n: (ciphers.length | number) }}
|
||||
{{ "inactive2faFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||
</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">
|
||||
<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>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// eslint-disable-next-line no-restricted-imports
|
||||
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 { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
|
@ -16,8 +17,11 @@ import { cipherData } from "./reports-ciphers.mock";
|
|||
describe("InactiveTwoFactorReportComponent", () => {
|
||||
let component: InactiveTwoFactorReportComponent;
|
||||
let fixture: ComponentFixture<InactiveTwoFactorReportComponent>;
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
|
||||
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.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
TestBed.configureTestingModule({
|
||||
|
@ -29,7 +33,7 @@ describe("InactiveTwoFactorReportComponent", () => {
|
|||
},
|
||||
{
|
||||
provide: OrganizationService,
|
||||
useValue: mock<OrganizationService>(),
|
||||
useValue: organizationService,
|
||||
},
|
||||
{
|
||||
provide: ModalService,
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
@ -26,8 +27,9 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
|||
modalService: ModalService,
|
||||
private logService: LogService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
i18nService: I18nService,
|
||||
) {
|
||||
super(modalService, passwordRepromptService, organizationService);
|
||||
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -45,6 +47,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
|||
const allCiphers = await this.getAllCiphers();
|
||||
const inactive2faCiphers: CipherView[] = [];
|
||||
const docs = new Map<string, string>();
|
||||
this.filterStatus = [0];
|
||||
|
||||
allCiphers.forEach((ciph) => {
|
||||
const { type, login, isDeleted, edit, id, viewPassword } = ciph;
|
||||
|
@ -58,6 +61,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
|||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < login.uris.length; i++) {
|
||||
const u = login.uris[i];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
protected getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllDecrypted();
|
||||
}
|
||||
|
||||
private async load2fa() {
|
||||
if (this.services.size > 0) {
|
||||
return;
|
||||
|
|
|
@ -16,9 +16,34 @@
|
|||
</app-callout>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{ 'reusedPasswordsFound' | i18n }}">
|
||||
{{ "reusedPasswordsFoundDesc" | i18n: (ciphers.length | number) }}
|
||||
{{ "reusedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||
</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">
|
||||
<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>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// eslint-disable-next-line no-restricted-imports
|
||||
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 { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
|
@ -15,8 +16,11 @@ import { ReusedPasswordsReportComponent } from "./reused-passwords-report.compon
|
|||
describe("ReusedPasswordsReportComponent", () => {
|
||||
let component: ReusedPasswordsReportComponent;
|
||||
let fixture: ComponentFixture<ReusedPasswordsReportComponent>;
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
|
||||
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.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
TestBed.configureTestingModule({
|
||||
|
@ -28,7 +32,7 @@ describe("ReusedPasswordsReportComponent", () => {
|
|||
},
|
||||
{
|
||||
provide: OrganizationService,
|
||||
useValue: mock<OrganizationService>(),
|
||||
useValue: organizationService,
|
||||
},
|
||||
{
|
||||
provide: ModalService,
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
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 { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
@ -22,8 +23,9 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
|
|||
protected organizationService: OrganizationService,
|
||||
modalService: ModalService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
i18nService: I18nService,
|
||||
) {
|
||||
super(modalService, passwordRepromptService, organizationService);
|
||||
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -34,6 +36,8 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
|
|||
const allCiphers = await this.getAllCiphers();
|
||||
const ciphersWithPasswords: CipherView[] = [];
|
||||
this.passwordUseMap = new Map<string, number>();
|
||||
this.filterStatus = [0];
|
||||
|
||||
allCiphers.forEach((ciph) => {
|
||||
const { type, login, isDeleted, edit, viewPassword } = ciph;
|
||||
if (
|
||||
|
@ -46,6 +50,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
|
|||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
ciphersWithPasswords.push(ciph);
|
||||
if (this.passwordUseMap.has(login.password)) {
|
||||
this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) + 1);
|
||||
|
@ -57,11 +62,8 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
|
|||
(c) =>
|
||||
this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password) > 1,
|
||||
);
|
||||
this.ciphers = reusedPasswordCiphers;
|
||||
}
|
||||
|
||||
protected getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllDecrypted();
|
||||
this.filterCiphersByOrg(reusedPasswordCiphers);
|
||||
}
|
||||
|
||||
protected canManageCipher(c: CipherView): boolean {
|
||||
|
|
|
@ -16,9 +16,33 @@
|
|||
</app-callout>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{ 'unsecuredWebsitesFound' | i18n }}">
|
||||
{{ "unsecuredWebsitesFoundDesc" | i18n: (ciphers.length | number) }}
|
||||
{{ "unsecuredWebsitesFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||
</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">
|
||||
<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>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// eslint-disable-next-line no-restricted-imports
|
||||
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 { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
|
@ -15,8 +16,11 @@ import { UnsecuredWebsitesReportComponent } from "./unsecured-websites-report.co
|
|||
describe("UnsecuredWebsitesReportComponent", () => {
|
||||
let component: UnsecuredWebsitesReportComponent;
|
||||
let fixture: ComponentFixture<UnsecuredWebsitesReportComponent>;
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
|
||||
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.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
TestBed.configureTestingModule({
|
||||
|
@ -28,7 +32,7 @@ describe("UnsecuredWebsitesReportComponent", () => {
|
|||
},
|
||||
{
|
||||
provide: OrganizationService,
|
||||
useValue: mock<OrganizationService>(),
|
||||
useValue: organizationService,
|
||||
},
|
||||
{
|
||||
provide: ModalService,
|
||||
|
|
|
@ -2,9 +2,9 @@ import { Component, OnInit } from "@angular/core";
|
|||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
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 { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { CipherReportComponent } from "./cipher-report.component";
|
||||
|
@ -21,8 +21,9 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl
|
|||
protected organizationService: OrganizationService,
|
||||
modalService: ModalService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
i18nService: I18nService,
|
||||
) {
|
||||
super(modalService, passwordRepromptService, organizationService);
|
||||
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -31,18 +32,15 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl
|
|||
|
||||
async setCiphers() {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
this.filterStatus = [0];
|
||||
const unsecuredCiphers = allCiphers.filter((c) => {
|
||||
if (c.type !== CipherType.Login || !c.login.hasUris || c.isDeleted) {
|
||||
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 this.cipherService.getAllDecrypted();
|
||||
return c.login.uris.some((u: any) => u.uri != null && u.uri.indexOf("http://") === 0);
|
||||
});
|
||||
|
||||
this.filterCiphersByOrg(unsecuredCiphers);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,32 @@
|
|||
</app-callout>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{ 'weakPasswordsFound' | i18n }}">
|
||||
{{ "weakPasswordsFoundDesc" | i18n: (ciphers.length | number) }}
|
||||
{{ "weakPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||
</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">
|
||||
<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>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
|
@ -17,9 +18,12 @@ describe("WeakPasswordsReportComponent", () => {
|
|||
let component: WeakPasswordsReportComponent;
|
||||
let fixture: ComponentFixture<WeakPasswordsReportComponent>;
|
||||
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
|
||||
beforeEach(() => {
|
||||
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.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
TestBed.configureTestingModule({
|
||||
|
@ -35,7 +39,7 @@ describe("WeakPasswordsReportComponent", () => {
|
|||
},
|
||||
{
|
||||
provide: OrganizationService,
|
||||
useValue: mock<OrganizationService>(),
|
||||
useValue: organizationService,
|
||||
},
|
||||
{
|
||||
provide: ModalService,
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
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 { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
@ -29,8 +30,9 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||
protected organizationService: OrganizationService,
|
||||
modalService: ModalService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
i18nService: I18nService,
|
||||
) {
|
||||
super(modalService, passwordRepromptService, organizationService);
|
||||
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -38,7 +40,10 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -55,6 +60,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasUserName = this.isUserNameNotEmpty(ciph);
|
||||
const cacheKey = this.getCacheKey(ciph);
|
||||
if (!this.passwordStrengthCache.has(cacheKey)) {
|
||||
|
@ -87,6 +93,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||
this.passwordStrengthCache.set(cacheKey, result.score);
|
||||
}
|
||||
const score = this.passwordStrengthCache.get(cacheKey);
|
||||
|
||||
if (score != null && score <= 2) {
|
||||
this.passwordStrengthMap.set(id, this.scoreKey(score));
|
||||
this.weakPasswordCiphers.push(ciph);
|
||||
|
@ -98,11 +105,8 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||
this.passwordStrengthCache.get(this.getCacheKey(b))
|
||||
);
|
||||
});
|
||||
this.ciphers = [...this.weakPasswordCiphers];
|
||||
}
|
||||
|
||||
protected getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllDecrypted();
|
||||
this.filterCiphersByOrg(this.weakPasswordCiphers);
|
||||
}
|
||||
|
||||
protected canManageCipher(c: CipherView): boolean {
|
||||
|
|
|
@ -32,7 +32,6 @@ export class VaultItemsComponent {
|
|||
@Input() showCollections: boolean;
|
||||
@Input() showGroups: boolean;
|
||||
@Input() useEvents: boolean;
|
||||
@Input() cloneableOrganizationCiphers: boolean;
|
||||
@Input() showPremiumFeatures: boolean;
|
||||
@Input() showBulkMove: boolean;
|
||||
@Input() showBulkTrashOptions: boolean;
|
||||
|
@ -160,10 +159,27 @@ export class VaultItemsComponent {
|
|||
}
|
||||
|
||||
protected canClone(vaultItem: VaultItem) {
|
||||
return (
|
||||
(vaultItem.cipher.organizationId && this.cloneableOrganizationCiphers) ||
|
||||
vaultItem.cipher.organizationId == null
|
||||
);
|
||||
if (vaultItem.cipher.organizationId == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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() {
|
||||
|
|
|
@ -49,7 +49,6 @@
|
|||
[showBulkMove]="showBulkMove"
|
||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||
[useEvents]="false"
|
||||
[cloneableOrganizationCiphers]="false"
|
||||
[showAdminActions]="false"
|
||||
(onEvent)="onVaultItemsEvent($event)"
|
||||
[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() {
|
||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
return super.loadCollections();
|
||||
|
|
|
@ -48,7 +48,6 @@
|
|||
[showBulkMove]="false"
|
||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||
[useEvents]="organization?.useEvents"
|
||||
[cloneableOrganizationCiphers]="true"
|
||||
[showAdminActions]="true"
|
||||
(onEvent)="onVaultItemsEvent($event)"
|
||||
[showBulkEditCollectionAccess]="organization?.flexibleCollections"
|
||||
|
|
|
@ -1809,12 +1809,16 @@
|
|||
"unsecuredWebsitesFound": {
|
||||
"message": "Unsecured websites found"
|
||||
},
|
||||
"unsecuredWebsitesFoundDesc": {
|
||||
"message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.",
|
||||
"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.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"vault": {
|
||||
"content": "$2",
|
||||
"example": "this will be 'vault' or 'vaults'"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1830,12 +1834,16 @@
|
|||
"inactive2faFound": {
|
||||
"message": "Logins without two-step login found"
|
||||
},
|
||||
"inactive2faFoundDesc": {
|
||||
"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.",
|
||||
"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.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"vault": {
|
||||
"content": "$2",
|
||||
"example": "this will be 'vault' or 'vaults'"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1854,12 +1862,16 @@
|
|||
"exposedPasswordsFound": {
|
||||
"message": "Exposed passwords found"
|
||||
},
|
||||
"exposedPasswordsFoundDesc": {
|
||||
"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.",
|
||||
"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.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"vault": {
|
||||
"content": "$2",
|
||||
"example": "this will be 'vault' or 'vaults'"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1887,12 +1899,16 @@
|
|||
"weakPasswordsFound": {
|
||||
"message": "Weak passwords found"
|
||||
},
|
||||
"weakPasswordsFoundDesc": {
|
||||
"message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.",
|
||||
"weakPasswordsFoundReportDesc": {
|
||||
"message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"vault": {
|
||||
"content": "$2",
|
||||
"example": "this will be 'vault' or 'vaults'"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1908,12 +1924,16 @@
|
|||
"reusedPasswordsFound": {
|
||||
"message": "Reused passwords found"
|
||||
},
|
||||
"reusedPasswordsFoundDesc": {
|
||||
"message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.",
|
||||
"reusedPasswordsFoundReportDesc": {
|
||||
"message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"vault": {
|
||||
"content": "$2",
|
||||
"example": "this will be 'vault' or 'vaults'"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -58,3 +58,12 @@ export class ServiceAccountPeopleAccessPoliciesView {
|
|||
userAccessPolicies: UserServiceAccountAccessPolicyView[];
|
||||
groupAccessPolicies: GroupServiceAccountAccessPolicyView[];
|
||||
}
|
||||
|
||||
export class ServiceAccountProjectPolicyPermissionDetailsView {
|
||||
accessPolicy: ServiceAccountProjectAccessPolicyView;
|
||||
hasPermission: boolean;
|
||||
}
|
||||
|
||||
export class ServiceAccountGrantedPoliciesView {
|
||||
grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsView[];
|
||||
}
|
||||
|
|
|
@ -1,17 +1,27 @@
|
|||
<div class="tw-mt-4 tw-w-2/5">
|
||||
<p class="tw-mt-6">
|
||||
{{ "machineAccountProjectsDescription" | i18n }}
|
||||
</p>
|
||||
<sm-access-selector
|
||||
[rows]="rows$ | async"
|
||||
granteeType="projects"
|
||||
[label]="'projects' | i18n"
|
||||
[hint]="'newSaSelectAccess' | i18n"
|
||||
[columnTitle]="'projects' | i18n"
|
||||
[emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n"
|
||||
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)"
|
||||
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)"
|
||||
(onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)"
|
||||
>
|
||||
</sm-access-selector>
|
||||
</div>
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner">
|
||||
<div class="tw-w-2/5">
|
||||
<p class="tw-mt-8" *ngIf="!loading">
|
||||
{{ "machineAccountProjectsDescription" | i18n }}
|
||||
</p>
|
||||
<sm-access-policy-selector
|
||||
[loading]="loading"
|
||||
formControlName="accessPolicies"
|
||||
[addButtonMode]="true"
|
||||
[items]="potentialGrantees"
|
||||
[label]="'projects' | i18n"
|
||||
[hint]="'newSaSelectAccess' | i18n"
|
||||
[columnTitle]="'projects' | i18n"
|
||||
[emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n"
|
||||
>
|
||||
</sm-access-policy-selector>
|
||||
<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 { 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 { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view";
|
||||
|
||||
import { ServiceAccountProjectAccessPolicyView } from "../../models/view/access-policy.view";
|
||||
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
|
||||
import { ServiceAccountGrantedPoliciesView } from "../../models/view/access-policy.view";
|
||||
import {
|
||||
AccessSelectorComponent,
|
||||
AccessSelectorRowView,
|
||||
} from "../../shared/access-policies/access-selector.component";
|
||||
ApItemValueType,
|
||||
convertToServiceAccountGrantedPoliciesView,
|
||||
} 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({
|
||||
selector: "sm-service-account-projects",
|
||||
templateUrl: "./service-account-projects.component.html",
|
||||
})
|
||||
export class ServiceAccountProjectsComponent implements OnInit, OnDestroy {
|
||||
private currentAccessPolicies: ApItemViewType[];
|
||||
private destroy$ = new Subject<void>();
|
||||
private serviceAccountId: string;
|
||||
private organizationId: string;
|
||||
private serviceAccountId: string;
|
||||
|
||||
protected rows$: Observable<AccessSelectorRowView[]> =
|
||||
this.accessPolicyService.serviceAccountGrantedPolicyChanges$.pipe(
|
||||
startWith(null),
|
||||
combineLatestWith(this.route.params),
|
||||
switchMap(([_, params]) =>
|
||||
this.accessPolicyService.getGrantedPolicies(params.serviceAccountId, params.organizationId),
|
||||
),
|
||||
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;
|
||||
});
|
||||
}),
|
||||
);
|
||||
private currentAccessPolicies$ = combineLatest([this.route.params]).pipe(
|
||||
switchMap(([params]) =>
|
||||
this.accessPolicyService
|
||||
.getServiceAccountGrantedPolicies(params.organizationId, params.serviceAccountId)
|
||||
.then((policies) => {
|
||||
return convertGrantedPoliciesToAccessPolicyItemViews(policies);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
protected handleCreateAccessPolicies(selected: SelectItemView[]) {
|
||||
const serviceAccountProjectAccessPolicyView = selected
|
||||
.filter((selection) => AccessSelectorComponent.getAccessItemType(selection) === "project")
|
||||
.map((filtered) => {
|
||||
const view = new ServiceAccountProjectAccessPolicyView();
|
||||
view.serviceAccountId = this.serviceAccountId;
|
||||
view.grantedProjectId = filtered.id;
|
||||
view.read = true;
|
||||
view.write = false;
|
||||
return view;
|
||||
});
|
||||
private potentialGrantees$ = combineLatest([this.route.params]).pipe(
|
||||
switchMap(([params]) =>
|
||||
this.accessPolicyService
|
||||
.getProjectsPotentialGrantees(params.organizationId)
|
||||
.then((grantees) => {
|
||||
return convertPotentialGranteesToApItemViewType(grantees);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return this.accessPolicyService.createGrantedPolicies(
|
||||
this.organizationId,
|
||||
this.serviceAccountId,
|
||||
serviceAccountProjectAccessPolicyView,
|
||||
);
|
||||
}
|
||||
protected formGroup = new FormGroup({
|
||||
accessPolicies: new FormControl([] as ApItemValueType[]),
|
||||
});
|
||||
|
||||
protected async handleUpdateAccessPolicy(policy: AccessSelectorRowView) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
protected loading = true;
|
||||
protected potentialGrantees: ApItemViewType[];
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private validationService: ValidationService,
|
||||
private accessPolicyService: AccessPolicyService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -92,10 +70,119 @@ export class ServiceAccountProjectsComponent implements OnInit, OnDestroy {
|
|||
this.organizationId = params.organizationId;
|
||||
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 {
|
||||
this.destroy$.next();
|
||||
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
|
||||
*ngFor="let item of selectionList.selectedItems; let i = index"
|
||||
[formGroupName]="i"
|
||||
[ngClass]="{ 'tw-text-muted': item.readOnly }"
|
||||
>
|
||||
<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 bitCell class="tw-max-w-sm tw-truncate">{{ item.labelName }}</td>
|
||||
<td bitCell class="tw-mb-auto tw-inline-block tw-w-auto">
|
||||
<select
|
||||
*ngIf="!staticPermission; else static"
|
||||
*ngIf="!staticPermission && !item.readOnly; else readOnly"
|
||||
bitInput
|
||||
formControlName="permission"
|
||||
(blur)="handleBlur()"
|
||||
|
@ -45,12 +48,20 @@
|
|||
{{ p.labelId | i18n }}
|
||||
</option>
|
||||
</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>
|
||||
<span>{{ staticPermission | i18n }}</span>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td bitCell class="tw-w-0">
|
||||
<button
|
||||
*ngIf="!item.readOnly"
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
buttonType="main"
|
||||
|
|
|
@ -35,6 +35,34 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn
|
|||
private notifyOnTouch: () => void;
|
||||
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.
|
||||
* 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),
|
||||
currentUser: new FormControl(currentUser),
|
||||
});
|
||||
|
||||
this.updateRowControlDisableState(fg, item);
|
||||
|
||||
return fg;
|
||||
}, this._itemComparator.bind(this));
|
||||
|
||||
|
@ -100,7 +131,13 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn
|
|||
|
||||
set items(val: ApItemViewType[]) {
|
||||
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(
|
||||
val.map((m) => {
|
||||
m.icon = m.icon ?? ApItemEnumUtil.itemIcon(m.type);
|
||||
|
@ -137,6 +174,9 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn
|
|||
} else {
|
||||
this.formGroup.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
|
||||
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 (selectedItems == null) {
|
||||
this.pauseChangeNotification = false;
|
||||
|
|
|
@ -347,6 +347,7 @@ function createApItemViewType(options: Partial<ApItemViewType> = {}) {
|
|||
labelName: options?.labelName ?? "test",
|
||||
type: options?.type ?? ApItemEnum.User,
|
||||
permission: options?.permission ?? ApPermissionEnum.CanRead,
|
||||
readOnly: options?.readOnly ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,9 @@ import {
|
|||
ServiceAccountPeopleAccessPoliciesView,
|
||||
UserServiceAccountAccessPolicyView,
|
||||
GroupServiceAccountAccessPolicyView,
|
||||
ServiceAccountGrantedPoliciesView,
|
||||
ServiceAccountProjectPolicyPermissionDetailsView,
|
||||
ServiceAccountProjectAccessPolicyView,
|
||||
} from "../../../../models/view/access-policy.view";
|
||||
|
||||
import { ApItemEnum } from "./enums/ap-item.enum";
|
||||
|
@ -76,3 +79,26 @@ export function convertToServiceAccountPeopleAccessPoliciesView(
|
|||
});
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { SelectItemView } from "@bitwarden/components";
|
|||
|
||||
import {
|
||||
ProjectPeopleAccessPoliciesView,
|
||||
ServiceAccountGrantedPoliciesView,
|
||||
ServiceAccountPeopleAccessPoliciesView,
|
||||
} from "../../../../models/view/access-policy.view";
|
||||
import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view";
|
||||
|
@ -13,6 +14,12 @@ import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.en
|
|||
export type ApItemViewType = SelectItemView & {
|
||||
accessPolicyId?: string;
|
||||
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;
|
||||
|
@ -47,6 +54,7 @@ export function convertToAccessPolicyItemViews(
|
|||
permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write),
|
||||
userId: policy.userId,
|
||||
currentUser: policy.currentUser,
|
||||
readOnly: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -60,12 +68,36 @@ export function convertToAccessPolicyItemViews(
|
|||
listName: policy.groupName,
|
||||
permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write),
|
||||
currentUserInGroup: policy.currentUserInGroup,
|
||||
readOnly: false,
|
||||
});
|
||||
});
|
||||
|
||||
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 convertPotentialGranteesToApItemViewType(
|
||||
grantees: PotentialGranteeView[],
|
||||
): ApItemViewType[] {
|
||||
|
@ -108,6 +140,7 @@ export function convertPotentialGranteesToApItemViewType(
|
|||
listName: listName,
|
||||
currentUserInGroup: granteeView.currentUserInGroup,
|
||||
currentUser: granteeView.currentUser,
|
||||
readOnly: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,15 +18,17 @@ import {
|
|||
UserProjectAccessPolicyView,
|
||||
UserServiceAccountAccessPolicyView,
|
||||
ServiceAccountPeopleAccessPoliciesView,
|
||||
ServiceAccountGrantedPoliciesView,
|
||||
ServiceAccountProjectPolicyPermissionDetailsView,
|
||||
} from "../../models/view/access-policy.view";
|
||||
import { PotentialGranteeView } from "../../models/view/potential-grantee.view";
|
||||
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 { 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 { AccessPolicyRequest } from "./models/requests/access-policy.request";
|
||||
import { GrantedPolicyRequest } from "./models/requests/granted-policy.request";
|
||||
import {
|
||||
GroupServiceAccountAccessPolicyResponse,
|
||||
UserServiceAccountAccessPolicyResponse,
|
||||
|
@ -36,28 +38,21 @@ import {
|
|||
} from "./models/responses/access-policy.response";
|
||||
import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response";
|
||||
import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-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 { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./models/responses/service-account-project-policy-permission-details.response";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class AccessPolicyService {
|
||||
private _projectAccessPolicyChanges$ = new Subject<ProjectAccessPoliciesView>();
|
||||
private _serviceAccountGrantedPolicyChanges$ = new Subject<
|
||||
ServiceAccountProjectAccessPolicyView[]
|
||||
>();
|
||||
|
||||
/**
|
||||
* Emits when a project access policy is created or deleted.
|
||||
*/
|
||||
readonly projectAccessPolicyChanges$ = this._projectAccessPolicyChanges$.asObservable();
|
||||
|
||||
/**
|
||||
* Emits when a service account granted policy is created or deleted.
|
||||
*/
|
||||
readonly serviceAccountGrantedPolicyChanges$ =
|
||||
this._serviceAccountGrantedPolicyChanges$.asObservable();
|
||||
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
protected apiService: ApiService,
|
||||
|
@ -68,44 +63,6 @@ export class AccessPolicyService {
|
|||
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(
|
||||
organizationId: string,
|
||||
projectId: string,
|
||||
|
@ -184,6 +141,40 @@ export class AccessPolicyService {
|
|||
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 createProjectAccessPolicies(
|
||||
organizationId: string,
|
||||
projectId: string,
|
||||
|
@ -206,7 +197,6 @@ export class AccessPolicyService {
|
|||
async deleteAccessPolicy(accessPolicyId: string): Promise<void> {
|
||||
await this.apiService.send("DELETE", "/access-policies/" + accessPolicyId, null, true, false);
|
||||
this._projectAccessPolicyChanges$.next(null);
|
||||
this._serviceAccountGrantedPolicyChanges$.next(null);
|
||||
}
|
||||
|
||||
async updateAccessPolicy(baseAccessPolicyView: BaseAccessPolicyView): Promise<void> {
|
||||
|
@ -222,6 +212,158 @@ 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 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(
|
||||
organizationId: string,
|
||||
projectAccessPoliciesResponse: ProjectAccessPoliciesResponse,
|
||||
|
@ -393,147 +535,4 @@ export class AccessPolicyService {
|
|||
currentUserInGroup: response.currentUserInGroup,
|
||||
};
|
||||
}
|
||||
|
||||
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 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;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { 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");
|
||||
}
|
||||
}
|
|
@ -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
|
||||
if (this.cloneMode && this.cipher?.login?.hasFido2Credentials) {
|
||||
|
|
Loading…
Reference in New Issue