From f684d1ba3141a2ca75fef7b8da2c8d8feaef3202 Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 28 Oct 2025 05:15:57 -0400 Subject: [PATCH] pam: rotation status & error improvements --- ...51028064623_pam-account-rotation-status.ts | 29 ++++++++++++++ backend/src/db/schemas/pam-accounts.ts | 4 +- .../routes/v1/pam-resource-routers/index.ts | 10 ++--- .../services/pam-account/pam-account-fns.ts | 38 +++++++++++++++++-- .../pam-account/pam-account-service.ts | 26 ++++++++++++- .../pam-resource/pam-resource-schemas.ts | 4 +- .../src/hooks/api/pam/types/base-account.ts | 2 + frontend/src/hooks/api/pam/types/index.ts | 4 +- .../PamAccountForm/MySQLAccountForm.tsx | 4 +- .../PamAccountForm/PostgresAccountForm.tsx | 6 +-- .../components/PamAccountRow.tsx | 21 ++++++++-- 11 files changed, 124 insertions(+), 24 deletions(-) create mode 100644 backend/src/db/migrations/20251028064623_pam-account-rotation-status.ts diff --git a/backend/src/db/migrations/20251028064623_pam-account-rotation-status.ts b/backend/src/db/migrations/20251028064623_pam-account-rotation-status.ts new file mode 100644 index 000000000..b3ad123e8 --- /dev/null +++ b/backend/src/db/migrations/20251028064623_pam-account-rotation-status.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.PamAccount, "rotationStatus"))) { + await knex.schema.alterTable(TableName.PamAccount, (t) => { + t.string("rotationStatus").nullable(); + }); + } + if (!(await knex.schema.hasColumn(TableName.PamAccount, "encryptedLastRotationMessage"))) { + await knex.schema.alterTable(TableName.PamAccount, (t) => { + t.binary("encryptedLastRotationMessage").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.PamAccount, "rotationStatus")) { + await knex.schema.alterTable(TableName.PamAccount, (t) => { + t.dropColumn("rotationStatus"); + }); + } + if (await knex.schema.hasColumn(TableName.PamAccount, "encryptedLastRotationMessage")) { + await knex.schema.alterTable(TableName.PamAccount, (t) => { + t.dropColumn("encryptedLastRotationMessage"); + }); + } +} diff --git a/backend/src/db/schemas/pam-accounts.ts b/backend/src/db/schemas/pam-accounts.ts index 7e78e0874..4f097a16d 100644 --- a/backend/src/db/schemas/pam-accounts.ts +++ b/backend/src/db/schemas/pam-accounts.ts @@ -21,7 +21,9 @@ export const PamAccountsSchema = z.object({ updatedAt: z.date(), rotationEnabled: z.boolean().default(false), rotationIntervalSeconds: z.number().nullable().optional(), - lastRotatedAt: z.date().nullable().optional() + lastRotatedAt: z.date().nullable().optional(), + rotationStatus: z.string().nullable().optional(), + encryptedLastRotationMessage: zodBuffer.nullable().optional() }); export type TPamAccounts = z.infer; diff --git a/backend/src/ee/routes/v1/pam-resource-routers/index.ts b/backend/src/ee/routes/v1/pam-resource-routers/index.ts index c6c0afcca..821532598 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/index.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/index.ts @@ -1,14 +1,14 @@ +import { + CreateMySQLResourceSchema, + MySQLResourceSchema, + UpdateMySQLResourceSchema +} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; import { CreatePostgresResourceSchema, SanitizedPostgresResourceSchema, UpdatePostgresResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; -import { - CreateMySQLResourceSchema, - MySQLResourceSchema, - UpdateMySQLResourceSchema -} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; import { registerPamResourceEndpoints } from "./pam-resource-endpoints"; diff --git a/backend/src/ee/services/pam-account/pam-account-fns.ts b/backend/src/ee/services/pam-account/pam-account-fns.ts index fdc440991..aae703eeb 100644 --- a/backend/src/ee/services/pam-account/pam-account-fns.ts +++ b/backend/src/ee/services/pam-account/pam-account-fns.ts @@ -45,17 +45,47 @@ export const decryptAccountCredentials = async ({ return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials; }; -export const decryptAccount = async ( +export const decryptAccountMessage = async ({ + projectId, + encryptedMessage, + kmsService +}: { + projectId: string; + encryptedMessage: Buffer; + kmsService: Pick; +}) => { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const decryptedPlainTextBlob = decryptor({ + cipherTextBlob: encryptedMessage + }); + + return decryptedPlainTextBlob.toString(); +}; + +export const decryptAccount = async < + T extends { encryptedCredentials: Buffer; encryptedLastRotationMessage?: Buffer | null } +>( account: T, projectId: string, kmsService: Pick -): Promise => { +): Promise => { return { ...account, credentials: await decryptAccountCredentials({ encryptedCredentials: account.encryptedCredentials, projectId, kmsService - }) - } as T & { credentials: TPamAccountCredentials }; + }), + lastRotationMessage: account.encryptedLastRotationMessage + ? await decryptAccountMessage({ + encryptedMessage: account.encryptedLastRotationMessage, + projectId, + kmsService + }) + : null + }; }; diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index fd3615013..12a0afa23 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -15,6 +15,7 @@ import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; import { ActorType } from "@app/services/auth/auth-type"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TUserDALFactory } from "@app/services/user/user-dal"; @@ -353,6 +354,7 @@ export const pamAccountServiceFactory = ({ TPamAccounts & { resource: Pick & { rotationCredentialsConfigured: boolean }; credentials: TPamAccountCredentials; + lastRotationMessage: string | null; } > = []; @@ -376,6 +378,7 @@ export const pamAccountServiceFactory = ({ ) { // Decrypt the account only if the user has permission to read it const decryptedAccount = await decryptAccount(account, account.projectId, kmsService); + decryptedAndPermittedAccounts.push({ ...decryptedAccount, resource: { @@ -619,7 +622,9 @@ export const pamAccountServiceFactory = ({ account.id, { encryptedCredentials, - lastRotatedAt: new Date() + lastRotatedAt: new Date(), + rotationStatus: "success", + encryptedLastRotationMessage: null }, tx ); @@ -645,6 +650,24 @@ export const pamAccountServiceFactory = ({ const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: account.projectId + }); + + const { cipherTextBlob: encryptedMessage } = encryptor({ + plainText: Buffer.from(errorMessage) + }); + + await pamAccountDAL.updateById( + account.id, + { + rotationStatus: "failed", + encryptedLastRotationMessage: encryptedMessage + }, + tx + ); + await auditLogService.createAuditLog({ projectId: account.projectId, actor: { @@ -662,7 +685,6 @@ export const pamAccountServiceFactory = ({ } } }); - throw error; // Rollback transaction } }) ); diff --git a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts index 7f6165d88..17ed1ccd1 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts @@ -33,7 +33,9 @@ export const BasePamAccountSchemaWithResource = BasePamAccountSchema.extend({ resourceType: true }).extend({ rotationCredentialsConfigured: z.boolean() - }) + }), + lastRotationMessage: z.string().nullable().optional(), + rotationStatus: z.string().nullable().optional() }); export const BaseCreatePamAccountSchema = z.object({ diff --git a/frontend/src/hooks/api/pam/types/base-account.ts b/frontend/src/hooks/api/pam/types/base-account.ts index 20cb7aa60..286c9389a 100644 --- a/frontend/src/hooks/api/pam/types/base-account.ts +++ b/frontend/src/hooks/api/pam/types/base-account.ts @@ -16,6 +16,8 @@ export interface TBasePamAccount { rotationEnabled: boolean; rotationIntervalSeconds?: number | null; lastRotatedAt?: string | null; + lastRotationMessage?: string | null; + rotationStatus?: string | null; createdAt: string; updatedAt: string; } diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts index 23cdf389d..1b1890cbd 100644 --- a/frontend/src/hooks/api/pam/types/index.ts +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -1,9 +1,9 @@ import { PamResourceType, PamSessionStatus } from "../enums"; -import { TPostgresAccount, TPostgresResource } from "./postgres-resource"; import { TMySQLAccount, TMySQLResource } from "./mysql-resource"; +import { TPostgresAccount, TPostgresResource } from "./postgres-resource"; -export * from "./postgres-resource"; export * from "./mysql-resource"; +export * from "./postgres-resource"; export type TPamResource = TPostgresResource | TMySQLResource; diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/MySQLAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/MySQLAccountForm.tsx index 131da7ef3..e30c79e24 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/MySQLAccountForm.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/MySQLAccountForm.tsx @@ -1,14 +1,14 @@ -import { zodResolver } from "@hookform/resolvers/zod"; import { FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Button, ModalClose } from "@app/components/v2"; import { PamResourceType, TMySQLAccount } from "@app/hooks/api/pam"; import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants"; -import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields"; import { BaseSqlAccountSchema } from "./shared/sql-account-schemas"; import { SqlAccountFields } from "./shared/SqlAccountFields"; +import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields"; type Props = { account?: TMySQLAccount; diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx index 6d361877e..e7d2d902b 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx @@ -1,6 +1,6 @@ -import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Button, ModalClose } from "@app/components/v2"; @@ -12,10 +12,10 @@ import { } from "@app/hooks/api/pam"; import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants"; -import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields"; -import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields"; import { BaseSqlAccountSchema } from "./shared/sql-account-schemas"; import { SqlAccountFields } from "./shared/SqlAccountFields"; +import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields"; +import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields"; type Props = { account?: TPostgresAccount; diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountRow.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountRow.tsx index 8e7507ac9..dde2bb4bc 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountRow.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountRow.tsx @@ -103,10 +103,23 @@ export const PamAccountRow = ({ )} {account.lastRotatedAt && ( - - - Rotated {formatDistance(new Date(), account.lastRotatedAt)} ago - + + + + Rotated {formatDistance(new Date(), account.lastRotatedAt)} ago + + )}