1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +00:00

[AC-2302] Extract device approve/deny logic into a service (#8818)

* [AC-2302] Move organization-auth-request.service to bit-common folder

* [AC-2302] Rename organization-auth-request.service to organization-auth-request-api.service

* [AC-2302] Move logic from component to organization-auth-request.service

* [AC-2302] Fix import path in OrganizationAuthRequestService

* [AC-2302] Move imports to OrganizationsModule and delete unused CoreOrganizationModule

* [AC-2302] Move the call to get userResetPasswordDetails into OrganizationAuthRequestService

* [AC-2302] Remove @Injectable() and manually configure dependencies

* [AC-2302] Add OrganizationAuthRequestService unit tests first draft

* [AC-2302] Refactor device-approvals.component.ts to remove unused imports

* [AC-2302] Set up jest on bit-common and add unit tests for OrganizationAuthRequestService

* [AC-2302] Add bit-common to jest.config.js

* [AC-2302] Update organizations.module.ts to include safeProviders declared in variable

* [AC-2302] Remove services and views folders from bit-common

* [AC-2302] Define path mapping

* Adjust an import path

The import path of `PendingAuthRequestView` in
`OrganizationAuthRequestApiService` was pointing to the wrong place. I
think this file was just recently moved, and the import didn't get
updated.

* Get paths working

* Fix import

* Update jest config to use ts-jest adn jsdom

* Copy-paste path mappings from bit-web

* Remove unnecessary test setup file

* Undo unnecessary change

* Fix remaining path mappings

* Remove Bitwarden License mapping from OSS code

* Fix bit-web so it uses its own tsconfig

* Fix import path

* Remove web-bit entrypoint from OSS tsconfig

* Make DeviceApprovalsComponent standalone

* Remove organization-auth-request-api.service export

* Remove OrganizationsRoutingModule from DeviceApprovalsComponent imports

* Remove CoreOrganizationModule from OrganizationsModule imports

* Remove NoItemsModule from OrganizationsModule imports

* Use ApiService from JslibServicesModule

* Update providers in device-approvals.component.ts

---------

Co-authored-by: Addison Beck <hello@addisonbeck.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
Rui Tomé
2024-05-20 11:18:23 +01:00
committed by GitHub
parent 4131aa9803
commit adf7a38f87
24 changed files with 340 additions and 103 deletions

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

@@ -1,11 +0,0 @@
export class AdminAuthRequestUpdateRequest {
/**
*
* @param requestApproved - Whether the request was approved/denied. If true, the key must be provided.
* @param encryptedUserKey The user key that has been encrypted with a device public key if the request was approved.
*/
constructor(
public requestApproved: boolean,
public encryptedUserKey?: string,
) {}
}

View File

@@ -1,6 +0,0 @@
export class BulkDenyAuthRequestsRequest {
private ids: string[];
constructor(authRequestIds: string[]) {
this.ids = authRequestIds;
}
}

View File

@@ -1,2 +0,0 @@
export * from "./pending-organization-auth-request.response";
export * from "./organization-auth-request.service";

View File

@@ -1,54 +0,0 @@
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 { PendingOrganizationAuthRequestResponse } from "./pending-organization-auth-request.response";
@Injectable()
export class OrganizationAuthRequestService {
constructor(private apiService: ApiService) {}
async listPendingRequests(organizationId: string): Promise<PendingAuthRequestView[]> {
const r = await this.apiService.send(
"GET",
`/organizations/${organizationId}/auth-requests`,
null,
true,
true,
);
const listResponse = new ListResponse(r, PendingOrganizationAuthRequestResponse);
return listResponse.data.map((ar) => PendingAuthRequestView.fromResponse(ar));
}
async denyPendingRequests(organizationId: string, ...requestIds: string[]): Promise<void> {
await this.apiService.send(
"POST",
`/organizations/${organizationId}/auth-requests/deny`,
new BulkDenyAuthRequestsRequest(requestIds),
true,
false,
);
}
async approvePendingRequest(
organizationId: string,
requestId: string,
encryptedKey: EncString,
): Promise<void> {
await this.apiService.send(
"POST",
`/organizations/${organizationId}/auth-requests/${requestId}`,
new AdminAuthRequestUpdateRequest(true, encryptedKey.encryptedString),
true,
false,
);
}
}

View File

@@ -1,26 +0,0 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
export class PendingOrganizationAuthRequestResponse extends BaseResponse {
id: string;
userId: string;
organizationUserId: string;
email: string;
publicKey: string;
requestDeviceIdentifier: string;
requestDeviceType: string;
requestIpAddress: string;
creationDate: string;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.userId = this.getResponseProperty("UserId");
this.organizationUserId = this.getResponseProperty("OrganizationUserId");
this.email = this.getResponseProperty("Email");
this.publicKey = this.getResponseProperty("PublicKey");
this.requestDeviceIdentifier = this.getResponseProperty("RequestDeviceIdentifier");
this.requestDeviceType = this.getResponseProperty("RequestDeviceType");
this.requestIpAddress = this.getResponseProperty("RequestIpAddress");
this.creationDate = this.getResponseProperty("CreationDate");
}
}

View File

@@ -1,23 +0,0 @@
import { View } from "@bitwarden/common/models/view/view";
import { PendingOrganizationAuthRequestResponse } from "../services/auth-requests";
export class PendingAuthRequestView implements View {
id: string;
userId: string;
organizationUserId: string;
email: string;
publicKey: string;
requestDeviceIdentifier: string;
requestDeviceType: string;
requestIpAddress: string;
creationDate: Date;
static fromResponse(response: PendingOrganizationAuthRequestResponse): PendingAuthRequestView {
const view = Object.assign(new PendingAuthRequestView(), response) as PendingAuthRequestView;
view.creationDate = new Date(response.creationDate);
return view;
}
}

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 {}