Merge branch 'main' into autofill/pm-7874-adding-login-on-current-cipher-view-causes-lost-state
This commit is contained in:
commit
3c8c5b691a
|
@ -106,7 +106,6 @@ import { DefaultConfigService } from "@bitwarden/common/platform/services/config
|
|||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
|
@ -219,6 +218,7 @@ import { BrowserCryptoService } from "../platform/services/browser-crypto.servic
|
|||
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
||||
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
|
||||
import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service";
|
||||
import { BrowserMultithreadEncryptServiceImplementation } from "../platform/services/browser-multithread-encrypt.service.implementation";
|
||||
import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service";
|
||||
import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service";
|
||||
import I18nService from "../platform/services/i18n.service";
|
||||
|
@ -475,14 +475,14 @@ export default class MainBackground {
|
|||
storageServiceProvider,
|
||||
);
|
||||
|
||||
this.encryptService =
|
||||
flagEnabled("multithreadDecryption") && BrowserApi.isManifestVersion(2)
|
||||
? new MultithreadEncryptServiceImplementation(
|
||||
this.cryptoFunctionService,
|
||||
this.logService,
|
||||
true,
|
||||
)
|
||||
: new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true);
|
||||
this.encryptService = flagEnabled("multithreadDecryption")
|
||||
? new BrowserMultithreadEncryptServiceImplementation(
|
||||
this.cryptoFunctionService,
|
||||
this.logService,
|
||||
true,
|
||||
this.offscreenDocumentService,
|
||||
)
|
||||
: new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true);
|
||||
|
||||
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
|
||||
storageServiceProvider,
|
||||
|
|
|
@ -2,6 +2,7 @@ export type OffscreenDocumentExtensionMessage = {
|
|||
[key: string]: any;
|
||||
command: string;
|
||||
text?: string;
|
||||
decryptRequest?: string;
|
||||
};
|
||||
|
||||
type OffscreenExtensionMessageEventParams = {
|
||||
|
@ -13,6 +14,7 @@ export type OffscreenDocumentExtensionMessageHandlers = {
|
|||
[key: string]: ({ message, sender }: OffscreenExtensionMessageEventParams) => any;
|
||||
offscreenCopyToClipboard: ({ message }: OffscreenExtensionMessageEventParams) => any;
|
||||
offscreenReadFromClipboard: () => any;
|
||||
offscreenDecryptItems: ({ message }: OffscreenExtensionMessageEventParams) => Promise<string>;
|
||||
};
|
||||
|
||||
export interface OffscreenDocument {
|
||||
|
|
|
@ -1,7 +1,25 @@
|
|||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import { flushPromises, sendExtensionRuntimeMessage } from "../../autofill/spec/testing-utils";
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
import BrowserClipboardService from "../services/browser-clipboard.service";
|
||||
|
||||
jest.mock(
|
||||
"@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation",
|
||||
() => ({
|
||||
MultithreadEncryptServiceImplementation: class MultithreadEncryptServiceImplementation {
|
||||
getDecryptedItemsFromWorker = async <T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
_key: SymmetricCryptoKey,
|
||||
): Promise<string> => JSON.stringify(items);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
describe("OffscreenDocument", () => {
|
||||
const browserApiMessageListenerSpy = jest.spyOn(BrowserApi, "messageListener");
|
||||
const browserClipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy");
|
||||
|
@ -60,5 +78,37 @@ describe("OffscreenDocument", () => {
|
|||
expect(browserClipboardServiceReadSpy).toHaveBeenCalledWith(window);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleOffscreenDecryptItems", () => {
|
||||
it("returns an empty array as a string if the decrypt request is not present in the message", async () => {
|
||||
let response: string | undefined;
|
||||
sendExtensionRuntimeMessage(
|
||||
{ command: "offscreenDecryptItems" },
|
||||
mock<chrome.runtime.MessageSender>(),
|
||||
(res: string) => (response = res),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(response).toBe("[]");
|
||||
});
|
||||
|
||||
it("decrypts the items and sends back the response as a string", async () => {
|
||||
const items = [{ id: "test" }];
|
||||
const key = { id: "test" };
|
||||
const decryptRequest = JSON.stringify({ items, key });
|
||||
let response: string | undefined;
|
||||
|
||||
sendExtensionRuntimeMessage(
|
||||
{ command: "offscreenDecryptItems", decryptRequest },
|
||||
mock<chrome.runtime.MessageSender>(),
|
||||
(res: string) => {
|
||||
response = res;
|
||||
},
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(response).toBe(JSON.stringify(items));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,21 +1,35 @@
|
|||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
import BrowserClipboardService from "../services/browser-clipboard.service";
|
||||
|
||||
import {
|
||||
OffscreenDocument as OffscreenDocumentInterface,
|
||||
OffscreenDocumentExtensionMessage,
|
||||
OffscreenDocumentExtensionMessageHandlers,
|
||||
OffscreenDocument as OffscreenDocumentInterface,
|
||||
} from "./abstractions/offscreen-document";
|
||||
|
||||
class OffscreenDocument implements OffscreenDocumentInterface {
|
||||
private consoleLogService: ConsoleLogService = new ConsoleLogService(false);
|
||||
private readonly consoleLogService: ConsoleLogService;
|
||||
private encryptService: MultithreadEncryptServiceImplementation;
|
||||
private readonly extensionMessageHandlers: OffscreenDocumentExtensionMessageHandlers = {
|
||||
offscreenCopyToClipboard: ({ message }) => this.handleOffscreenCopyToClipboard(message),
|
||||
offscreenReadFromClipboard: () => this.handleOffscreenReadFromClipboard(),
|
||||
offscreenDecryptItems: ({ message }) => this.handleOffscreenDecryptItems(message),
|
||||
};
|
||||
|
||||
constructor() {
|
||||
const cryptoFunctionService = new WebCryptoFunctionService(self);
|
||||
this.consoleLogService = new ConsoleLogService(false);
|
||||
this.encryptService = new MultithreadEncryptServiceImplementation(
|
||||
cryptoFunctionService,
|
||||
this.consoleLogService,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the offscreen document extension.
|
||||
*/
|
||||
|
@ -39,6 +53,23 @@ class OffscreenDocument implements OffscreenDocumentInterface {
|
|||
return await BrowserClipboardService.read(self);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the items in the message using the encrypt service.
|
||||
*
|
||||
* @param message - The extension message containing the items to decrypt
|
||||
*/
|
||||
private async handleOffscreenDecryptItems(
|
||||
message: OffscreenDocumentExtensionMessage,
|
||||
): Promise<string> {
|
||||
const { decryptRequest } = message;
|
||||
if (!decryptRequest) {
|
||||
return "[]";
|
||||
}
|
||||
|
||||
const request = JSON.parse(decryptRequest);
|
||||
return await this.encryptService.getDecryptedItemsFromWorker(request.items, request.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the listener for extension messages.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { InitializerKey } from "@bitwarden/common/platform/services/cryptography/initializer-key";
|
||||
import { makeStaticByteArray } from "@bitwarden/common/spec";
|
||||
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
import { OffscreenDocumentService } from "../offscreen-document/abstractions/offscreen-document";
|
||||
|
||||
import { BrowserMultithreadEncryptServiceImplementation } from "./browser-multithread-encrypt.service.implementation";
|
||||
|
||||
describe("BrowserMultithreadEncryptServiceImplementation", () => {
|
||||
let cryptoFunctionServiceMock: MockProxy<CryptoFunctionService>;
|
||||
let logServiceMock: MockProxy<LogService>;
|
||||
let offscreenDocumentServiceMock: MockProxy<OffscreenDocumentService>;
|
||||
let encryptService: BrowserMultithreadEncryptServiceImplementation;
|
||||
const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
|
||||
const sendMessageWithResponseSpy = jest.spyOn(BrowserApi, "sendMessageWithResponse");
|
||||
const encType = EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 100), encType);
|
||||
const items: Decryptable<InitializerMetadata>[] = [
|
||||
{
|
||||
decrypt: jest.fn(),
|
||||
initializerKey: InitializerKey.Cipher,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoFunctionServiceMock = mock<CryptoFunctionService>();
|
||||
logServiceMock = mock<LogService>();
|
||||
offscreenDocumentServiceMock = mock<OffscreenDocumentService>({
|
||||
withDocument: jest.fn((_, __, callback) => callback() as any),
|
||||
});
|
||||
encryptService = new BrowserMultithreadEncryptServiceImplementation(
|
||||
cryptoFunctionServiceMock,
|
||||
logServiceMock,
|
||||
false,
|
||||
offscreenDocumentServiceMock,
|
||||
);
|
||||
manifestVersionSpy.mockReturnValue(3);
|
||||
sendMessageWithResponseSpy.mockResolvedValue(JSON.stringify([]));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("decrypts items using web workers if the chrome.offscreen API is not supported", async () => {
|
||||
manifestVersionSpy.mockReturnValue(2);
|
||||
|
||||
await encryptService.decryptItems([], key);
|
||||
|
||||
expect(offscreenDocumentServiceMock.withDocument).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("decrypts items using the chrome.offscreen API if it is supported", async () => {
|
||||
sendMessageWithResponseSpy.mockResolvedValue(JSON.stringify(items));
|
||||
|
||||
await encryptService.decryptItems(items, key);
|
||||
|
||||
expect(offscreenDocumentServiceMock.withDocument).toHaveBeenCalledWith(
|
||||
[chrome.offscreen.Reason.WORKERS],
|
||||
"Use web worker to decrypt items.",
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenDecryptItems", {
|
||||
decryptRequest: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it("returns an empty array if the passed items are not defined", async () => {
|
||||
const result = await encryptService.decryptItems(null, key);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty array if the offscreen document message returns an empty value", async () => {
|
||||
sendMessageWithResponseSpy.mockResolvedValue("");
|
||||
|
||||
const result = await encryptService.decryptItems(items, key);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty array if the offscreen document message returns an empty array", async () => {
|
||||
sendMessageWithResponseSpy.mockResolvedValue("[]");
|
||||
|
||||
const result = await encryptService.decryptItems(items, key);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
import { OffscreenDocumentService } from "../offscreen-document/abstractions/offscreen-document";
|
||||
|
||||
export class BrowserMultithreadEncryptServiceImplementation extends MultithreadEncryptServiceImplementation {
|
||||
constructor(
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
logService: LogService,
|
||||
logMacFailures: boolean,
|
||||
private offscreenDocumentService: OffscreenDocumentService,
|
||||
) {
|
||||
super(cryptoFunctionService, logService, logMacFailures);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles decryption of items, will use the offscreen document if supported.
|
||||
*
|
||||
* @param items - The items to decrypt.
|
||||
* @param key - The key to use for decryption.
|
||||
*/
|
||||
async decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<T[]> {
|
||||
if (!this.isOffscreenDocumentSupported()) {
|
||||
return await super.decryptItems(items, key);
|
||||
}
|
||||
|
||||
return await this.decryptItemsInOffscreenDocument(items, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts items using the offscreen document api.
|
||||
*
|
||||
* @param items - The items to decrypt.
|
||||
* @param key - The key to use for decryption.
|
||||
*/
|
||||
private async decryptItemsInOffscreenDocument<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<T[]> {
|
||||
if (items == null || items.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const request = {
|
||||
id: Utils.newGuid(),
|
||||
items: items,
|
||||
key: key,
|
||||
};
|
||||
|
||||
const response = await this.offscreenDocumentService.withDocument(
|
||||
[chrome.offscreen.Reason.WORKERS],
|
||||
"Use web worker to decrypt items.",
|
||||
async () => {
|
||||
return (await BrowserApi.sendMessageWithResponse("offscreenDecryptItems", {
|
||||
decryptRequest: JSON.stringify(request),
|
||||
})) as string;
|
||||
},
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const responseItems = JSON.parse(response);
|
||||
if (responseItems?.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.initializeItems(responseItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the offscreen document api is supported.
|
||||
*/
|
||||
private isOffscreenDocumentSupported() {
|
||||
return (
|
||||
BrowserApi.isManifestVersion(3) &&
|
||||
typeof chrome !== "undefined" &&
|
||||
typeof chrome.offscreen !== "undefined"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -67,3 +67,7 @@ export class ServiceAccountProjectPolicyPermissionDetailsView {
|
|||
export class ServiceAccountGrantedPoliciesView {
|
||||
grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsView[];
|
||||
}
|
||||
|
||||
export class ProjectServiceAccountsAccessPoliciesView {
|
||||
serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[];
|
||||
}
|
||||
|
|
|
@ -1,17 +1,27 @@
|
|||
<div class="tw-w-2/5">
|
||||
<p class="tw-mt-8">
|
||||
{{ "projectMachineAccountsDescription" | i18n }}
|
||||
</p>
|
||||
<sm-access-selector
|
||||
[rows]="rows$ | async"
|
||||
granteeType="serviceAccounts"
|
||||
[label]="'machineAccounts' | i18n"
|
||||
[hint]="'projectMachineAccountsSelectHint' | i18n"
|
||||
[columnTitle]="'machineAccounts' | i18n"
|
||||
[emptyMessage]="'projectEmptyMachineAccountAccessPolicies' | 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">
|
||||
{{ "projectMachineAccountsDescription" | i18n }}
|
||||
</p>
|
||||
<sm-access-policy-selector
|
||||
[loading]="loading"
|
||||
formControlName="accessPolicies"
|
||||
[addButtonMode]="true"
|
||||
[items]="items"
|
||||
[label]="'machineAccounts' | i18n"
|
||||
[hint]="'projectMachineAccountsSelectHint' | i18n"
|
||||
[columnTitle]="'machineAccounts' | i18n"
|
||||
[emptyMessage]="'projectEmptyMachineAccountAccessPolicies' | 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,93 +1,69 @@
|
|||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup } from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { 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";
|
||||
|
||||
import { ProjectServiceAccountsAccessPoliciesView } from "../../models/view/access-policy.view";
|
||||
import {
|
||||
ProjectAccessPoliciesView,
|
||||
ServiceAccountProjectAccessPolicyView,
|
||||
} from "../../models/view/access-policy.view";
|
||||
ApItemValueType,
|
||||
convertToProjectServiceAccountsAccessPoliciesView,
|
||||
} from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type";
|
||||
import {
|
||||
ApItemViewType,
|
||||
convertPotentialGranteesToApItemViewType,
|
||||
convertProjectServiceAccountsViewToApItemViews,
|
||||
} from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type";
|
||||
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
|
||||
import {
|
||||
AccessSelectorComponent,
|
||||
AccessSelectorRowView,
|
||||
} from "../../shared/access-policies/access-selector.component";
|
||||
|
||||
@Component({
|
||||
selector: "sm-project-service-accounts",
|
||||
templateUrl: "./project-service-accounts.component.html",
|
||||
})
|
||||
export class ProjectServiceAccountsComponent implements OnInit, OnDestroy {
|
||||
private currentAccessPolicies: ApItemViewType[];
|
||||
private destroy$ = new Subject<void>();
|
||||
private organizationId: string;
|
||||
private projectId: string;
|
||||
|
||||
protected rows$: Observable<AccessSelectorRowView[]> =
|
||||
this.accessPolicyService.projectAccessPolicyChanges$.pipe(
|
||||
startWith(null),
|
||||
switchMap(() =>
|
||||
this.accessPolicyService.getProjectAccessPolicies(this.organizationId, this.projectId),
|
||||
),
|
||||
map((policies) =>
|
||||
policies.serviceAccountAccessPolicies.map((policy) => ({
|
||||
type: "serviceAccount",
|
||||
name: policy.serviceAccountName,
|
||||
id: policy.serviceAccountId,
|
||||
accessPolicyId: policy.id,
|
||||
read: policy.read,
|
||||
write: policy.write,
|
||||
icon: AccessSelectorComponent.serviceAccountIcon,
|
||||
static: false,
|
||||
})),
|
||||
),
|
||||
);
|
||||
private currentAccessPolicies$ = combineLatest([this.route.params]).pipe(
|
||||
switchMap(([params]) =>
|
||||
this.accessPolicyService
|
||||
.getProjectServiceAccountsAccessPolicies(params.organizationId, params.projectId)
|
||||
.then((policies) => {
|
||||
return convertProjectServiceAccountsViewToApItemViews(policies);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
protected async handleUpdateAccessPolicy(policy: AccessSelectorRowView) {
|
||||
try {
|
||||
return await this.accessPolicyService.updateAccessPolicy(
|
||||
AccessSelectorComponent.getBaseAccessPolicyView(policy),
|
||||
);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
}
|
||||
private potentialGrantees$ = combineLatest([this.route.params]).pipe(
|
||||
switchMap(([params]) =>
|
||||
this.accessPolicyService
|
||||
.getServiceAccountsPotentialGrantees(params.organizationId)
|
||||
.then((grantees) => {
|
||||
return convertPotentialGranteesToApItemViewType(grantees);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
protected handleCreateAccessPolicies(selected: SelectItemView[]) {
|
||||
const projectAccessPoliciesView = new ProjectAccessPoliciesView();
|
||||
projectAccessPoliciesView.serviceAccountAccessPolicies = selected
|
||||
.filter(
|
||||
(selection) => AccessSelectorComponent.getAccessItemType(selection) === "serviceAccount",
|
||||
)
|
||||
.map((filtered) => {
|
||||
const view = new ServiceAccountProjectAccessPolicyView();
|
||||
view.grantedProjectId = this.projectId;
|
||||
view.serviceAccountId = filtered.id;
|
||||
view.read = true;
|
||||
view.write = false;
|
||||
return view;
|
||||
});
|
||||
protected formGroup = new FormGroup({
|
||||
accessPolicies: new FormControl([] as ApItemValueType[]),
|
||||
});
|
||||
|
||||
return this.accessPolicyService.createProjectAccessPolicies(
|
||||
this.organizationId,
|
||||
this.projectId,
|
||||
projectAccessPoliciesView,
|
||||
);
|
||||
}
|
||||
|
||||
protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) {
|
||||
try {
|
||||
await this.accessPolicyService.deleteAccessPolicy(policy.accessPolicyId);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
}
|
||||
protected loading = true;
|
||||
protected potentialGrantees: ApItemViewType[];
|
||||
protected items: ApItemViewType[];
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private validationService: ValidationService,
|
||||
private accessPolicyService: AccessPolicyService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -95,10 +71,97 @@ export class ProjectServiceAccountsComponent implements OnInit, OnDestroy {
|
|||
this.organizationId = params.organizationId;
|
||||
this.projectId = params.projectId;
|
||||
});
|
||||
|
||||
combineLatest([this.potentialGrantees$, this.currentAccessPolicies$])
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(([potentialGrantees, currentAccessPolicies]) => {
|
||||
this.potentialGrantees = potentialGrantees;
|
||||
this.items = this.getItems(potentialGrantees, currentAccessPolicies);
|
||||
this.setSelected(currentAccessPolicies);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
if (this.isFormInvalid()) {
|
||||
return;
|
||||
}
|
||||
const formValues = this.formGroup.value.accessPolicies;
|
||||
this.formGroup.disable();
|
||||
|
||||
try {
|
||||
const accessPoliciesView = await this.updateProjectServiceAccountsAccessPolicies(
|
||||
this.organizationId,
|
||||
this.projectId,
|
||||
formValues,
|
||||
);
|
||||
|
||||
const updatedView = convertProjectServiceAccountsViewToApItemViews(accessPoliciesView);
|
||||
this.items = this.getItems(this.potentialGrantees, updatedView);
|
||||
this.setSelected(updatedView);
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("projectAccessUpdated"),
|
||||
);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
this.setSelected(this.currentAccessPolicies);
|
||||
}
|
||||
this.formGroup.enable();
|
||||
};
|
||||
|
||||
private setSelected(policiesToSelect: ApItemViewType[]) {
|
||||
this.loading = true;
|
||||
this.currentAccessPolicies = policiesToSelect;
|
||||
if (policiesToSelect != undefined) {
|
||||
// Must detect changes so that AccessSelector @Inputs() are aware of the latest
|
||||
// potentialGrantees, otherwise no selected values will be patched below
|
||||
this.changeDetectorRef.detectChanges();
|
||||
this.formGroup.patchValue({
|
||||
accessPolicies: policiesToSelect.map((m) => ({
|
||||
type: m.type,
|
||||
id: m.id,
|
||||
permission: m.permission,
|
||||
})),
|
||||
});
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
private isFormInvalid(): boolean {
|
||||
this.formGroup.markAllAsTouched();
|
||||
return this.formGroup.invalid;
|
||||
}
|
||||
|
||||
private async updateProjectServiceAccountsAccessPolicies(
|
||||
organizationId: string,
|
||||
projectId: string,
|
||||
selectedPolicies: ApItemValueType[],
|
||||
): Promise<ProjectServiceAccountsAccessPoliciesView> {
|
||||
const view = convertToProjectServiceAccountsAccessPoliciesView(projectId, selectedPolicies);
|
||||
return await this.accessPolicyService.putProjectServiceAccountsAccessPolicies(
|
||||
organizationId,
|
||||
projectId,
|
||||
view,
|
||||
);
|
||||
}
|
||||
|
||||
private getItems(potentialGrantees: ApItemViewType[], currentAccessPolicies: ApItemViewType[]) {
|
||||
// If the user doesn't have access to the service account, they won't be in the potentialGrantees list.
|
||||
// Add them to the potentialGrantees list if they are selected.
|
||||
const items = [...potentialGrantees];
|
||||
for (const policy of currentAccessPolicies) {
|
||||
const exists = potentialGrantees.some((grantee) => grantee.id === policy.id);
|
||||
if (!exists) {
|
||||
items.push(policy);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
ServiceAccountGrantedPoliciesView,
|
||||
ServiceAccountProjectPolicyPermissionDetailsView,
|
||||
ServiceAccountProjectAccessPolicyView,
|
||||
ProjectServiceAccountsAccessPoliciesView,
|
||||
} from "../../../../models/view/access-policy.view";
|
||||
|
||||
import { ApItemEnum } from "./enums/ap-item.enum";
|
||||
|
@ -102,3 +103,23 @@ export function convertToServiceAccountGrantedPoliciesView(
|
|||
|
||||
return view;
|
||||
}
|
||||
|
||||
export function convertToProjectServiceAccountsAccessPoliciesView(
|
||||
projectId: string,
|
||||
selectedPolicyValues: ApItemValueType[],
|
||||
): ProjectServiceAccountsAccessPoliciesView {
|
||||
const view = new ProjectServiceAccountsAccessPoliciesView();
|
||||
|
||||
view.serviceAccountAccessPolicies = selectedPolicyValues
|
||||
.filter((x) => x.type == ApItemEnum.ServiceAccount)
|
||||
.map((filtered) => {
|
||||
const policyView = new ServiceAccountProjectAccessPolicyView();
|
||||
policyView.serviceAccountId = filtered.id;
|
||||
policyView.grantedProjectId = projectId;
|
||||
policyView.read = ApPermissionEnumUtil.toRead(filtered.permission);
|
||||
policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission);
|
||||
return policyView;
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { SelectItemView } from "@bitwarden/components";
|
|||
import {
|
||||
ProjectPeopleAccessPoliciesView,
|
||||
ServiceAccountGrantedPoliciesView,
|
||||
ProjectServiceAccountsAccessPoliciesView,
|
||||
ServiceAccountPeopleAccessPoliciesView,
|
||||
} from "../../../../models/view/access-policy.view";
|
||||
import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view";
|
||||
|
@ -98,6 +99,29 @@ export function convertGrantedPoliciesToAccessPolicyItemViews(
|
|||
return accessPolicies;
|
||||
}
|
||||
|
||||
export function convertProjectServiceAccountsViewToApItemViews(
|
||||
value: ProjectServiceAccountsAccessPoliciesView,
|
||||
): ApItemViewType[] {
|
||||
const accessPolicies: ApItemViewType[] = [];
|
||||
|
||||
value.serviceAccountAccessPolicies.forEach((accessPolicyView) => {
|
||||
accessPolicies.push({
|
||||
type: ApItemEnum.ServiceAccount,
|
||||
icon: ApItemEnumUtil.itemIcon(ApItemEnum.ServiceAccount),
|
||||
id: accessPolicyView.serviceAccountId,
|
||||
accessPolicyId: accessPolicyView.id,
|
||||
labelName: accessPolicyView.serviceAccountName,
|
||||
listName: accessPolicyView.serviceAccountName,
|
||||
permission: ApPermissionEnumUtil.toApPermissionEnum(
|
||||
accessPolicyView.read,
|
||||
accessPolicyView.write,
|
||||
),
|
||||
readOnly: false,
|
||||
});
|
||||
});
|
||||
return accessPolicies;
|
||||
}
|
||||
|
||||
export function convertPotentialGranteesToApItemViewType(
|
||||
grantees: PotentialGranteeView[],
|
||||
): ApItemViewType[] {
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
UserServiceAccountAccessPolicyView,
|
||||
ServiceAccountPeopleAccessPoliciesView,
|
||||
ServiceAccountGrantedPoliciesView,
|
||||
ProjectServiceAccountsAccessPoliciesView,
|
||||
ServiceAccountProjectPolicyPermissionDetailsView,
|
||||
} from "../../models/view/access-policy.view";
|
||||
import { PotentialGranteeView } from "../../models/view/potential-grantee.view";
|
||||
|
@ -29,6 +30,7 @@ import { ServiceAccountGrantedPoliciesRequest } from "../access-policies/models/
|
|||
|
||||
import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request";
|
||||
import { AccessPolicyRequest } from "./models/requests/access-policy.request";
|
||||
import { ProjectServiceAccountsAccessPoliciesRequest } from "./models/requests/project-service-accounts-access-policies.request";
|
||||
import {
|
||||
GroupServiceAccountAccessPolicyResponse,
|
||||
UserServiceAccountAccessPolicyResponse,
|
||||
|
@ -38,6 +40,7 @@ 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 { ProjectServiceAccountsAccessPoliciesResponse } from "./models/responses/project-service-accounts-access-policies.response";
|
||||
import { ServiceAccountGrantedPoliciesPermissionDetailsResponse } from "./models/responses/service-account-granted-policies-permission-details.response";
|
||||
import { ServiceAccountPeopleAccessPoliciesResponse } from "./models/responses/service-account-people-access-policies.response";
|
||||
import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./models/responses/service-account-project-policy-permission-details.response";
|
||||
|
@ -175,6 +178,40 @@ export class AccessPolicyService {
|
|||
return await this.createServiceAccountGrantedPoliciesView(result, organizationId);
|
||||
}
|
||||
|
||||
async getProjectServiceAccountsAccessPolicies(
|
||||
organizationId: string,
|
||||
projectId: string,
|
||||
): Promise<ProjectServiceAccountsAccessPoliciesView> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/projects/" + projectId + "/access-policies/service-accounts",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
const result = new ProjectServiceAccountsAccessPoliciesResponse(r);
|
||||
return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId);
|
||||
}
|
||||
|
||||
async putProjectServiceAccountsAccessPolicies(
|
||||
organizationId: string,
|
||||
projectId: string,
|
||||
policies: ProjectServiceAccountsAccessPoliciesView,
|
||||
): Promise<ProjectServiceAccountsAccessPoliciesView> {
|
||||
const request = this.getProjectServiceAccountsAccessPoliciesRequest(policies);
|
||||
const r = await this.apiService.send(
|
||||
"PUT",
|
||||
"/projects/" + projectId + "/access-policies/service-accounts",
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
const result = new ProjectServiceAccountsAccessPoliciesResponse(r);
|
||||
return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId);
|
||||
}
|
||||
|
||||
async createProjectAccessPolicies(
|
||||
organizationId: string,
|
||||
projectId: string,
|
||||
|
@ -325,6 +362,18 @@ export class AccessPolicyService {
|
|||
return request;
|
||||
}
|
||||
|
||||
private getProjectServiceAccountsAccessPoliciesRequest(
|
||||
policies: ProjectServiceAccountsAccessPoliciesView,
|
||||
): ProjectServiceAccountsAccessPoliciesRequest {
|
||||
const request = new ProjectServiceAccountsAccessPoliciesRequest();
|
||||
|
||||
request.serviceAccountAccessPolicyRequests = policies.serviceAccountAccessPolicies.map((ap) => {
|
||||
return this.getAccessPolicyRequest(ap.serviceAccountId, ap);
|
||||
});
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private async createServiceAccountGrantedPoliciesView(
|
||||
response: ServiceAccountGrantedPoliciesPermissionDetailsResponse,
|
||||
organizationId: string,
|
||||
|
@ -535,4 +584,19 @@ export class AccessPolicyService {
|
|||
currentUserInGroup: response.currentUserInGroup,
|
||||
};
|
||||
}
|
||||
|
||||
private async createProjectServiceAccountsAccessPoliciesView(
|
||||
response: ProjectServiceAccountsAccessPoliciesResponse,
|
||||
organizationId: string,
|
||||
): Promise<ProjectServiceAccountsAccessPoliciesView> {
|
||||
const orgKey = await this.getOrganizationKey(organizationId);
|
||||
|
||||
const view = new ProjectServiceAccountsAccessPoliciesView();
|
||||
view.serviceAccountAccessPolicies = await Promise.all(
|
||||
response.serviceAccountAccessPolicies.map(async (ap) => {
|
||||
return await this.createServiceAccountProjectAccessPolicyView(orgKey, ap);
|
||||
}),
|
||||
);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { AccessPolicyRequest } from "./access-policy.request";
|
||||
|
||||
export class ProjectServiceAccountsAccessPoliciesRequest {
|
||||
serviceAccountAccessPolicyRequests?: AccessPolicyRequest[];
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
|
||||
import { ServiceAccountProjectAccessPolicyResponse } from "./access-policy.response";
|
||||
|
||||
export class ProjectServiceAccountsAccessPoliciesResponse extends BaseResponse {
|
||||
serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyResponse[];
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
const serviceAccountAccessPolicies = this.getResponseProperty("ServiceAccountAccessPolicies");
|
||||
this.serviceAccountAccessPolicies = serviceAccountAccessPolicies.map(
|
||||
(k: any) => new ServiceAccountProjectAccessPolicyResponse(k),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -19,17 +19,36 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
|
|||
private clear$ = new Subject<void>();
|
||||
|
||||
/**
|
||||
* Sends items to a web worker to decrypt them.
|
||||
* This utilises multithreading to decrypt items faster without interrupting other operations (e.g. updating UI).
|
||||
* Decrypts items using a web worker if the environment supports it.
|
||||
* Will fall back to the main thread if the window object is not available.
|
||||
*/
|
||||
async decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<T[]> {
|
||||
if (typeof window === "undefined") {
|
||||
return super.decryptItems(items, key);
|
||||
}
|
||||
|
||||
if (items == null || items.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const decryptedItems = await this.getDecryptedItemsFromWorker(items, key);
|
||||
const parsedItems = JSON.parse(decryptedItems);
|
||||
|
||||
return this.initializeItems(parsedItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends items to a web worker to decrypt them. This utilizes multithreading to decrypt items
|
||||
* faster without interrupting other operations (e.g. updating UI). This method returns values
|
||||
* prior to deserialization to support forwarding results to another party
|
||||
*/
|
||||
async getDecryptedItemsFromWorker<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<string> {
|
||||
this.logService.info("Starting decryption using multithreading");
|
||||
|
||||
this.worker ??= new Worker(
|
||||
|
@ -53,19 +72,20 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
|
|||
return await firstValueFrom(
|
||||
fromEvent(this.worker, "message").pipe(
|
||||
filter((response: MessageEvent) => response.data?.id === request.id),
|
||||
map((response) => JSON.parse(response.data.items)),
|
||||
map((items) =>
|
||||
items.map((jsonItem: Jsonify<T>) => {
|
||||
const initializer = getClassInitializer<T>(jsonItem.initializerKey);
|
||||
return initializer(jsonItem);
|
||||
}),
|
||||
),
|
||||
map((response) => response.data.items),
|
||||
takeUntil(this.clear$),
|
||||
defaultIfEmpty([]),
|
||||
defaultIfEmpty("[]"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
protected initializeItems<T extends InitializerMetadata>(items: Jsonify<T>[]): T[] {
|
||||
return items.map((jsonItem: Jsonify<T>) => {
|
||||
const initializer = getClassInitializer<T>(jsonItem.initializerKey);
|
||||
return initializer(jsonItem);
|
||||
});
|
||||
}
|
||||
|
||||
private clear() {
|
||||
this.clear$.next();
|
||||
this.worker?.terminate();
|
||||
|
|
Loading…
Reference in New Issue