This commit is contained in:
Rui Tomé 2024-05-17 23:13:56 +02:00 committed by GitHub
commit fec46fdc08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 340 additions and 103 deletions

View File

@ -262,6 +262,12 @@
}
]
}
},
{
"files": ["bitwarden_license/bit-common/src/**/*.ts"],
"rules": {
"no-restricted-imports": ["error", { "patterns": ["@bitwarden/bit-common/*", "src/**/*"] }]
}
}
]
}

View File

@ -27,12 +27,7 @@
"strictTemplates": true,
"preserveWhitespaces": true
},
"files": [
"src/polyfills.ts",
"src/main.ts",
"../../bitwarden_license/bit-web/src/main.ts",
"src/theme.ts"
],
"files": ["src/polyfills.ts", "src/main.ts", "src/theme.ts"],
"include": [
"src/connectors/*.ts",
"src/**/*.stories.ts",

View File

@ -0,0 +1,16 @@
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
const sharedConfig = require("../../libs/shared/jest.config.angular");
/** @type {import('jest').Config} */
module.exports = {
...sharedConfig,
displayName: "bit-common tests",
preset: "ts-jest",
testEnvironment: "jsdom",
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/",
}),
};

View File

@ -1,17 +1,13 @@
import { Injectable } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { PendingAuthRequestView } from "../../views/pending-auth-request.view";
import { AdminAuthRequestUpdateRequest } from "./admin-auth-request-update.request";
import { BulkDenyAuthRequestsRequest } from "./bulk-deny-auth-requests.request";
import { PendingAuthRequestView } from "./pending-auth-request.view";
import { PendingOrganizationAuthRequestResponse } from "./pending-organization-auth-request.response";
@Injectable()
export class OrganizationAuthRequestService {
export class OrganizationAuthRequestApiService {
constructor(private apiService: ApiService) {}
async listPendingRequests(organizationId: string): Promise<PendingAuthRequestView[]> {

View File

@ -0,0 +1,132 @@
import { MockProxy, mock } from "jest-mock-extended";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { OrganizationAuthRequestApiService } from "./organization-auth-request-api.service";
import { OrganizationAuthRequestService } from "./organization-auth-request.service";
import { PendingAuthRequestView } from "./pending-auth-request.view";
describe("OrganizationAuthRequestService", () => {
let organizationAuthRequestApiService: MockProxy<OrganizationAuthRequestApiService>;
let cryptoService: MockProxy<CryptoService>;
let organizationUserService: MockProxy<OrganizationUserService>;
let organizationAuthRequestService: OrganizationAuthRequestService;
beforeEach(() => {
organizationAuthRequestApiService = mock<OrganizationAuthRequestApiService>();
cryptoService = mock<CryptoService>();
organizationUserService = mock<OrganizationUserService>();
organizationAuthRequestService = new OrganizationAuthRequestService(
organizationAuthRequestApiService,
cryptoService,
organizationUserService,
);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("listPendingRequests", () => {
it("should return a list of pending auth requests", async () => {
jest.spyOn(organizationAuthRequestApiService, "listPendingRequests");
const organizationId = "organizationId";
const pendingAuthRequest = new PendingAuthRequestView();
pendingAuthRequest.id = "requestId1";
pendingAuthRequest.userId = "userId1";
pendingAuthRequest.organizationUserId = "userId1";
pendingAuthRequest.email = "email1";
pendingAuthRequest.publicKey = "publicKey1";
pendingAuthRequest.requestDeviceIdentifier = "requestDeviceIdentifier1";
pendingAuthRequest.requestDeviceType = "requestDeviceType1";
pendingAuthRequest.requestIpAddress = "requestIpAddress1";
pendingAuthRequest.creationDate = new Date();
const mockPendingAuthRequests = [pendingAuthRequest];
organizationAuthRequestApiService.listPendingRequests
.calledWith(organizationId)
.mockResolvedValue(mockPendingAuthRequests);
const result = await organizationAuthRequestService.listPendingRequests(organizationId);
expect(result).toHaveLength(1);
expect(result).toEqual(mockPendingAuthRequests);
expect(organizationAuthRequestApiService.listPendingRequests).toHaveBeenCalledWith(
organizationId,
);
});
it("should return an empty list", async () => {
jest.spyOn(organizationAuthRequestApiService, "listPendingRequests");
const invalidOrganizationId = "invalidOrganizationId";
const result =
await organizationAuthRequestService.listPendingRequests("invalidOrganizationId");
expect(result).toBeUndefined();
expect(organizationAuthRequestApiService.listPendingRequests).toHaveBeenCalledWith(
invalidOrganizationId,
);
});
});
describe("denyPendingRequests", () => {
it("should deny the specified pending auth requests", async () => {
jest.spyOn(organizationAuthRequestApiService, "denyPendingRequests");
await organizationAuthRequestService.denyPendingRequests(
"organizationId",
"requestId1",
"requestId2",
);
expect(organizationAuthRequestApiService.denyPendingRequests).toHaveBeenCalledWith(
"organizationId",
"requestId1",
"requestId2",
);
});
});
describe("approvePendingRequest", () => {
it("should approve the specified pending auth request", async () => {
jest.spyOn(organizationAuthRequestApiService, "approvePendingRequest");
const organizationId = "organizationId";
const organizationUserResetPasswordDetailsResponse =
new OrganizationUserResetPasswordDetailsResponse({
resetPasswordKey: "resetPasswordKey",
encryptedPrivateKey: "encryptedPrivateKey",
});
organizationUserService.getOrganizationUserResetPasswordDetails.mockResolvedValue(
organizationUserResetPasswordDetailsResponse,
);
const encryptedUserKey = new EncString("encryptedUserKey");
cryptoService.rsaDecrypt.mockResolvedValue(new Uint8Array(32));
cryptoService.rsaEncrypt.mockResolvedValue(encryptedUserKey);
const mockPendingAuthRequest = new PendingAuthRequestView();
mockPendingAuthRequest.id = "requestId1";
mockPendingAuthRequest.organizationUserId = "organizationUserId1";
mockPendingAuthRequest.publicKey = "publicKey1";
await organizationAuthRequestService.approvePendingRequest(
organizationId,
mockPendingAuthRequest,
);
expect(organizationAuthRequestApiService.approvePendingRequest).toHaveBeenCalledWith(
organizationId,
mockPendingAuthRequest.id,
encryptedUserKey,
);
});
});
});

View File

@ -0,0 +1,81 @@
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { OrganizationAuthRequestApiService } from "./organization-auth-request-api.service";
import { PendingAuthRequestView } from "./pending-auth-request.view";
export class OrganizationAuthRequestService {
constructor(
private organizationAuthRequestApiService: OrganizationAuthRequestApiService,
private cryptoService: CryptoService,
private organizationUserService: OrganizationUserService,
) {}
async listPendingRequests(organizationId: string): Promise<PendingAuthRequestView[]> {
return await this.organizationAuthRequestApiService.listPendingRequests(organizationId);
}
async denyPendingRequests(organizationId: string, ...requestIds: string[]): Promise<void> {
await this.organizationAuthRequestApiService.denyPendingRequests(organizationId, ...requestIds);
}
async approvePendingRequest(organizationId: string, authRequest: PendingAuthRequestView) {
const details = await this.organizationUserService.getOrganizationUserResetPasswordDetails(
organizationId,
authRequest.organizationUserId,
);
if (details == null || details.resetPasswordKey == null) {
throw new Error(
"The user must be enrolled in account recovery (password reset) in order for the request to be approved.",
);
}
const encryptedKey = await this.getEncryptedUserKey(
organizationId,
authRequest.publicKey,
details,
);
await this.organizationAuthRequestApiService.approvePendingRequest(
organizationId,
authRequest.id,
encryptedKey,
);
}
/**
* Creates a copy of the user key that has been encrypted with the provided device's public key.
* @param organizationId
* @param devicePublicKey
* @param resetPasswordDetails
* @private
*/
private async getEncryptedUserKey(
organizationId: string,
devicePublicKey: string,
resetPasswordDetails: OrganizationUserResetPasswordDetailsResponse,
): Promise<EncString> {
const encryptedUserKey = resetPasswordDetails.resetPasswordKey;
const encryptedOrgPrivateKey = resetPasswordDetails.encryptedPrivateKey;
const devicePubKey = Utils.fromB64ToArray(devicePublicKey);
// Decrypt Organization's encrypted Private Key with org key
const orgSymKey = await this.cryptoService.getOrgKey(organizationId);
const decOrgPrivateKey = await this.cryptoService.decryptToBytes(
new EncString(encryptedOrgPrivateKey),
orgSymKey,
);
// Decrypt user key with decrypted org private key
const decValue = await this.cryptoService.rsaDecrypt(encryptedUserKey, decOrgPrivateKey);
const userKey = new SymmetricCryptoKey(decValue);
// Re-encrypt user Key with the Device Public Key
return await this.cryptoService.rsaEncrypt(userKey.key, devicePubKey);
}
}

View File

@ -1,6 +1,6 @@
import { View } from "@bitwarden/common/models/view/view";
import { PendingOrganizationAuthRequestResponse } from "../services/auth-requests";
import { PendingOrganizationAuthRequestResponse } from ".";
export class PendingAuthRequestView implements View {
id: string;

View File

@ -0,0 +1,24 @@
{
"extends": "../../libs/shared/tsconfig.libs",
"include": ["src", "spec"],
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@bitwarden/admin-console": ["../../libs/admin-console/src"],
"@bitwarden/angular/*": ["../../libs/angular/src/*"],
"@bitwarden/auth": ["../../libs/auth/src"],
"@bitwarden/billing": ["../../libs/billing/src"],
"@bitwarden/common/*": ["../../libs/common/src/*"],
"@bitwarden/components": ["../../libs/components/src"],
"@bitwarden/vault-export-core": [
"../../libs/tools/export/vault-export/vault-export-core/src"
],
"@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-core/src"],
"@bitwarden/platform": ["../../libs/platform/src"],
"@bitwarden/vault": ["../../libs/vault/src"],
"@bitwarden/web-vault/*": ["../../apps/web/src/*"],
"@bitwarden/bit-common/*": ["../bit-common/src/*"]
}
}
}

View File

@ -0,0 +1,3 @@
{
"extends": "./tsconfig.json"
}

View File

@ -1,8 +0,0 @@
import { NgModule } from "@angular/core";
import { OrganizationAuthRequestService } from "./services/auth-requests";
@NgModule({
providers: [OrganizationAuthRequestService],
})
export class CoreOrganizationModule {}

View File

@ -1 +0,0 @@
export * from "./core-organization.module";

View File

@ -2,25 +2,37 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { BehaviorSubject, Subject, switchMap, takeUntil, tap } from "rxjs";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import { OrganizationAuthRequestApiService } from "@bitwarden/bit-common/admin-console/auth-requests/organization-auth-request-api.service";
import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests/organization-auth-request.service";
import { PendingAuthRequestView } from "@bitwarden/bit-common/admin-console/auth-requests/pending-auth-request.view";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { TableDataSource } from "@bitwarden/components";
import { TableDataSource, NoItemsModule } from "@bitwarden/components";
import { Devices } from "@bitwarden/web-vault/app/admin-console/icons";
import { OrganizationAuthRequestService } from "../../core/services/auth-requests";
import { PendingAuthRequestView } from "../../core/views/pending-auth-request.view";
import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared";
import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module";
@Component({
selector: "app-org-device-approvals",
templateUrl: "./device-approvals.component.html",
standalone: true,
providers: [
safeProvider({
provide: OrganizationAuthRequestApiService,
deps: [ApiService],
}),
safeProvider({
provide: OrganizationAuthRequestService,
deps: [OrganizationAuthRequestApiService, CryptoService, OrganizationUserService],
}),
] satisfies SafeProvider[],
imports: [SharedModule, NoItemsModule, LooseComponentsModule],
})
export class DeviceApprovalsComponent implements OnInit, OnDestroy {
tableDataSource = new TableDataSource<PendingAuthRequestView>();
@ -35,8 +47,6 @@ export class DeviceApprovalsComponent implements OnInit, OnDestroy {
constructor(
private organizationAuthRequestService: OrganizationAuthRequestService,
private organizationUserService: OrganizationUserService,
private cryptoService: CryptoService,
private route: ActivatedRoute,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
@ -64,65 +74,26 @@ export class DeviceApprovalsComponent implements OnInit, OnDestroy {
});
}
/**
* Creates a copy of the user key that has been encrypted with the provided device's public key.
* @param devicePublicKey
* @param resetPasswordDetails
* @private
*/
private async getEncryptedUserKey(
devicePublicKey: string,
resetPasswordDetails: OrganizationUserResetPasswordDetailsResponse,
): Promise<EncString> {
const encryptedUserKey = resetPasswordDetails.resetPasswordKey;
const encryptedOrgPrivateKey = resetPasswordDetails.encryptedPrivateKey;
const devicePubKey = Utils.fromB64ToArray(devicePublicKey);
// Decrypt Organization's encrypted Private Key with org key
const orgSymKey = await this.cryptoService.getOrgKey(this.organizationId);
const decOrgPrivateKey = await this.cryptoService.decryptToBytes(
new EncString(encryptedOrgPrivateKey),
orgSymKey,
);
// Decrypt user key with decrypted org private key
const decValue = await this.cryptoService.rsaDecrypt(encryptedUserKey, decOrgPrivateKey);
const userKey = new SymmetricCryptoKey(decValue);
// Re-encrypt user Key with the Device Public Key
return await this.cryptoService.rsaEncrypt(userKey.key, devicePubKey);
}
async approveRequest(authRequest: PendingAuthRequestView) {
await this.performAsyncAction(async () => {
const details = await this.organizationUserService.getOrganizationUserResetPasswordDetails(
this.organizationId,
authRequest.organizationUserId,
);
try {
await this.organizationAuthRequestService.approvePendingRequest(
this.organizationId,
authRequest,
);
// The user must be enrolled in account recovery (password reset) in order for the request to be approved.
if (details == null || details.resetPasswordKey == null) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("loginRequestApproved"),
);
} catch (error) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("resetPasswordDetailsError"),
);
return;
}
const encryptedKey = await this.getEncryptedUserKey(authRequest.publicKey, details);
await this.organizationAuthRequestService.approvePendingRequest(
this.organizationId,
authRequest.id,
encryptedKey,
);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("loginRequestApproved"),
);
});
}

View File

@ -9,7 +9,6 @@ import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/admin-cons
import { SsoComponent } from "../../auth/sso/sso.component";
import { DeviceApprovalsComponent } from "./manage/device-approvals/device-approvals.component";
import { DomainVerificationComponent } from "./manage/domain-verification/domain-verification.component";
import { ScimComponent } from "./manage/scim.component";
@ -55,7 +54,10 @@ const routes: Routes = [
},
{
path: "device-approvals",
component: DeviceApprovalsComponent,
loadComponent: () =>
import("./manage/device-approvals/device-approvals.component").then(
(mod) => mod.DeviceApprovalsComponent,
),
canActivate: [OrganizationPermissionsGuard],
data: {
organizationPermissions: (org: Organization) => org.canManageDeviceApprovals,

View File

@ -1,32 +1,22 @@
import { NgModule } from "@angular/core";
import { NoItemsModule } from "@bitwarden/components";
import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared";
import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module";
import { SsoComponent } from "../../auth/sso/sso.component";
import { CoreOrganizationModule } from "./core";
import { DeviceApprovalsComponent } from "./manage/device-approvals/device-approvals.component";
import { DomainAddEditDialogComponent } from "./manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component";
import { DomainVerificationComponent } from "./manage/domain-verification/domain-verification.component";
import { ScimComponent } from "./manage/scim.component";
import { OrganizationsRoutingModule } from "./organizations-routing.module";
@NgModule({
imports: [
SharedModule,
CoreOrganizationModule,
OrganizationsRoutingModule,
NoItemsModule,
LooseComponentsModule,
],
imports: [SharedModule, OrganizationsRoutingModule, LooseComponentsModule],
declarations: [
SsoComponent,
ScimComponent,
DomainVerificationComponent,
DomainAddEditDialogComponent,
DeviceApprovalsComponent,
],
})
export class OrganizationsModule {}

View File

@ -1,21 +1,43 @@
{
"extends": "../../apps/web/tsconfig",
"compilerOptions": {
"baseUrl": ".",
"module": "ES2020",
"resolveJsonModule": true,
"paths": {
"@bitwarden/admin-console": ["../../libs/admin-console/src"],
"@bitwarden/angular/*": ["../../libs/angular/src/*"],
"@bitwarden/auth": ["../../libs/auth/src"],
"@bitwarden/auth/common": ["../../libs/auth/src/common"],
"@bitwarden/auth/angular": ["../../libs/auth/src/angular"],
"@bitwarden/billing": ["../../libs/billing/src"],
"@bitwarden/common/*": ["../../libs/common/src/*"],
"@bitwarden/components": ["../../libs/components/src"],
"@bitwarden/vault-export-core": [
"../../libs/tools/export/vault-export/vault-export-core/src"
],
"@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-core/src"],
"@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"],
"@bitwarden/importer/core": ["../../libs/importer/src"],
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/platform": ["../../libs/platform/src"],
"@bitwarden/vault": ["../../libs/vault/src"],
"@bitwarden/web-vault/*": ["../../apps/web/src/*"]
"@bitwarden/web-vault/*": ["../../apps/web/src/*"],
"@bitwarden/bit-common/*": ["../bit-common/src/*"]
}
},
"include": ["src/**/*.stories.ts"]
"files": [
"../../apps/web/src/polyfills.ts",
"../../apps/web/src/main.ts",
"../../apps/web/src/theme.ts",
"../../bitwarden_license/bit-web/src/main.ts"
],
"include": [
"../../apps/web/src/connectors/*.ts",
"../../apps/web/src/**/*.stories.ts",
"../../apps/web/src/**/*.spec.ts",
"../../libs/common/src/platform/services/**/*.worker.ts",
"src/**/*.stories.ts"
]
}

View File

@ -4,7 +4,7 @@ const webpackConfig = require("../../apps/web/webpack.config");
webpackConfig.entry["app/main"] = "../../bitwarden_license/bit-web/src/main.ts";
webpackConfig.plugins[webpackConfig.plugins.length - 1] = new AngularWebpackPlugin({
tsConfigPath: "tsconfig.json",
tsconfig: "../../bitwarden_license/bit-web/tsconfig.json",
entryModule: "bitwarden_license/src/app/app.module#AppModule",
sourceMap: true,
});

View File

@ -32,6 +32,10 @@
"name": "libs",
"path": "libs",
},
{
"name": "common (bit)",
"path": "bitwarden_license/bit-common",
},
],
"settings": {
"eslint.options": {

View File

@ -22,6 +22,7 @@ module.exports = {
"<rootDir>/apps/web/jest.config.js",
"<rootDir>/bitwarden_license/bit-web/jest.config.js",
"<rootDir>/bitwarden_license/bit-cli/jest.config.js",
"<rootDir>/bitwarden_license/bit-common/jest.config.js",
"<rootDir>/libs/admin-console/jest.config.js",
"<rootDir>/libs/angular/jest.config.js",

View File

@ -27,7 +27,8 @@
"@bitwarden/importer/ui": ["./libs/importer/src/components"],
"@bitwarden/platform": ["./libs/platform/src"],
"@bitwarden/node/*": ["./libs/node/src/*"],
"@bitwarden/vault": ["./libs/vault/src"]
"@bitwarden/vault": ["./libs/vault/src"],
"@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"]
},
"plugins": [
{

View File

@ -29,7 +29,8 @@
"@bitwarden/platform": ["./libs/platform/src"],
"@bitwarden/node/*": ["./libs/node/src/*"],
"@bitwarden/web-vault/*": ["./apps/web/src/*"],
"@bitwarden/vault": ["./libs/vault/src"]
"@bitwarden/vault": ["./libs/vault/src"],
"@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"]
},
"plugins": [
{
@ -42,7 +43,8 @@
"apps/web/src/**/*",
"apps/browser/src/**/*",
"libs/*/src/**/*",
"bitwarden_license/bit-web/src/**/*"
"bitwarden_license/bit-web/src/**/*",
"bitwarden_license/bit-common/src/**/*"
],
"exclude": [
"apps/web/src/**/*.spec.ts",