greptile review comment

This commit is contained in:
x032205
2025-10-21 19:47:58 -04:00
parent 85daaf806e
commit 0a9d391732
9 changed files with 135 additions and 93 deletions
@@ -3,18 +3,21 @@ import { Knex } from "knex";
import { TableName } from "../schemas"; import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
if ( if (!(await knex.schema.hasColumn(TableName.PamAccount, "rotationEnabled"))) {
!(await knex.schema.hasColumn(TableName.PamAccount, "rotationEnabled")) &&
!(await knex.schema.hasColumn(TableName.PamAccount, "rotationIntervalSeconds")) &&
!(await knex.schema.hasColumn(TableName.PamAccount, "lastRotatedAt"))
) {
await knex.schema.alterTable(TableName.PamAccount, (t) => { await knex.schema.alterTable(TableName.PamAccount, (t) => {
t.boolean("rotationEnabled").notNullable().defaultTo(false); t.boolean("rotationEnabled").notNullable().defaultTo(false);
});
}
if (!(await knex.schema.hasColumn(TableName.PamAccount, "rotationIntervalSeconds"))) {
await knex.schema.alterTable(TableName.PamAccount, (t) => {
t.integer("rotationIntervalSeconds").nullable(); t.integer("rotationIntervalSeconds").nullable();
});
}
if (!(await knex.schema.hasColumn(TableName.PamAccount, "lastRotatedAt"))) {
await knex.schema.alterTable(TableName.PamAccount, (t) => {
t.timestamp("lastRotatedAt").nullable(); t.timestamp("lastRotatedAt").nullable();
}); });
} }
if (!(await knex.schema.hasColumn(TableName.PamResource, "encryptedRotationAccountCredentials"))) { if (!(await knex.schema.hasColumn(TableName.PamResource, "encryptedRotationAccountCredentials"))) {
await knex.schema.alterTable(TableName.PamResource, (t) => { await knex.schema.alterTable(TableName.PamResource, (t) => {
t.binary("encryptedRotationAccountCredentials").nullable(); t.binary("encryptedRotationAccountCredentials").nullable();
@@ -28,16 +31,19 @@ export async function down(knex: Knex): Promise<void> {
t.dropColumn("encryptedRotationAccountCredentials"); t.dropColumn("encryptedRotationAccountCredentials");
}); });
} }
if (await knex.schema.hasColumn(TableName.PamAccount, "rotationEnabled")) {
if (
(await knex.schema.hasColumn(TableName.PamAccount, "rotationEnabled")) &&
(await knex.schema.hasColumn(TableName.PamAccount, "rotationIntervalSeconds")) &&
(await knex.schema.hasColumn(TableName.PamAccount, "lastRotatedAt"))
) {
await knex.schema.alterTable(TableName.PamAccount, (t) => { await knex.schema.alterTable(TableName.PamAccount, (t) => {
t.dropColumn("lastRotatedAt");
t.dropColumn("rotationIntervalSeconds");
t.dropColumn("rotationEnabled"); t.dropColumn("rotationEnabled");
}); });
} }
if (await knex.schema.hasColumn(TableName.PamAccount, "rotationIntervalSeconds")) {
await knex.schema.alterTable(TableName.PamAccount, (t) => {
t.dropColumn("rotationIntervalSeconds");
});
}
if (await knex.schema.hasColumn(TableName.PamAccount, "lastRotatedAt")) {
await knex.schema.alterTable(TableName.PamAccount, (t) => {
t.dropColumn("lastRotatedAt");
});
}
} }
@@ -50,6 +50,7 @@ export const pamAccountDALFactory = (db: TDbClient) => {
.innerJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`) .innerJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`)
.whereNotNull(`${TableName.PamResource}.encryptedRotationAccountCredentials`) .whereNotNull(`${TableName.PamResource}.encryptedRotationAccountCredentials`)
.whereNotNull(`${TableName.PamAccount}.rotationIntervalSeconds`) .whereNotNull(`${TableName.PamAccount}.rotationIntervalSeconds`)
.where(`${TableName.PamAccount}.rotationEnabled`, true)
.whereRaw( .whereRaw(
`COALESCE("${TableName.PamAccount}"."lastRotatedAt", "${TableName.PamAccount}"."createdAt") + "${TableName.PamAccount}"."rotationIntervalSeconds" * interval '1 second' < NOW()` `COALESCE("${TableName.PamAccount}"."lastRotatedAt", "${TableName.PamAccount}"."createdAt") + "${TableName.PamAccount}"."rotationIntervalSeconds" * interval '1 second' < NOW()`
) )
@@ -85,6 +85,12 @@ export const pamAccountServiceFactory = ({
}); });
} }
if (rotationEnabled && (rotationIntervalSeconds === undefined || rotationIntervalSeconds === null)) {
throw new BadRequestError({
message: "Rotation interval must be defined when rotation is enabled."
});
}
const resource = await pamResourceDAL.findById(resourceId); const resource = await pamResourceDAL.findById(resourceId);
if (!resource) throw new NotFoundError({ message: `Resource with ID '${resourceId}' not found` }); if (!resource) throw new NotFoundError({ message: `Resource with ID '${resourceId}' not found` });
@@ -568,87 +574,97 @@ export const pamAccountServiceFactory = ({
for (let i = 0; i < accounts.length; i += ROTATION_CONCURRENCY_LIMIT) { for (let i = 0; i < accounts.length; i += ROTATION_CONCURRENCY_LIMIT) {
const batch = accounts.slice(i, i + ROTATION_CONCURRENCY_LIMIT); const batch = accounts.slice(i, i + ROTATION_CONCURRENCY_LIMIT);
const rotationPromises = batch.map(async (account) => { const rotationPromises = batch.map(async (account) =>
let logResourceType = "unknown"; pamAccountDAL.transaction(async (tx) => {
try { let logResourceType = "unknown";
const resource = await pamResourceDAL.findById(account.resourceId); try {
if (!resource || !resource.encryptedRotationAccountCredentials) return; const resource = await pamResourceDAL.findById(account.resourceId, tx);
logResourceType = resource.resourceType; if (!resource || !resource.encryptedRotationAccountCredentials) return;
logResourceType = resource.resourceType;
const { connectionDetails, rotationAccountCredentials, gatewayId, resourceType } = await decryptResource( const { connectionDetails, rotationAccountCredentials, gatewayId, resourceType } = await decryptResource(
resource, resource,
account.projectId, account.projectId,
kmsService kmsService
); );
if (!rotationAccountCredentials) return; if (!rotationAccountCredentials) return;
const accountCredentials = await decryptAccountCredentials({ const accountCredentials = await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials, encryptedCredentials: account.encryptedCredentials,
projectId: account.projectId, projectId: account.projectId,
kmsService kmsService
}); });
const factory = PAM_RESOURCE_FACTORY_MAP[resourceType as PamResource]( const factory = PAM_RESOURCE_FACTORY_MAP[resourceType as PamResource](
resourceType as PamResource, resourceType as PamResource,
connectionDetails, connectionDetails,
gatewayId, gatewayId,
gatewayV2Service gatewayV2Service
); );
const newCredentials = await factory.rotateAccountCredentials(rotationAccountCredentials, accountCredentials); const newCredentials = await factory.rotateAccountCredentials(
rotationAccountCredentials,
accountCredentials
);
const encryptedCredentials = await encryptAccountCredentials({ const encryptedCredentials = await encryptAccountCredentials({
credentials: newCredentials, credentials: newCredentials,
projectId: account.projectId, projectId: account.projectId,
kmsService kmsService
}); });
await pamAccountDAL.updateById(account.id, { await pamAccountDAL.updateById(
encryptedCredentials, account.id,
lastRotatedAt: new Date() {
}); encryptedCredentials,
lastRotatedAt: new Date()
},
tx
);
await auditLogService.createAuditLog({ await auditLogService.createAuditLog({
projectId: account.projectId, projectId: account.projectId,
actor: { actor: {
type: ActorType.PLATFORM, type: ActorType.PLATFORM,
metadata: {} metadata: {}
}, },
event: { event: {
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION, type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION,
metadata: { metadata: {
accountId: account.id, accountId: account.id,
accountName: account.name, accountName: account.name,
resourceId: resource.id, resourceId: resource.id,
resourceType: logResourceType resourceType: logResourceType
}
} }
} });
}); } catch (error) {
} catch (error) { logger.error(error, `Failed to rotate credentials for account [accountId=${account.id}]`);
logger.error(error, `Failed to rotate credentials for account [accountId=${account.id}]`);
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
await auditLogService.createAuditLog({ await auditLogService.createAuditLog({
projectId: account.projectId, projectId: account.projectId,
actor: { actor: {
type: ActorType.PLATFORM, type: ActorType.PLATFORM,
metadata: {} metadata: {}
}, },
event: { event: {
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED, type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED,
metadata: { metadata: {
accountId: account.id, accountId: account.id,
accountName: account.name, accountName: account.name,
resourceId: account.resourceId, resourceId: account.resourceId,
resourceType: logResourceType, resourceType: logResourceType,
errorMessage errorMessage
}
} }
} });
}); throw error; // Rollback transaction
} }
}); })
);
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await Promise.all(rotationPromises); await Promise.all(rotationPromises);
View File
+1
View File
@@ -0,0 +1 @@
export const UNCHANGED_PASSWORD_SENTINEL = "__INFISICAL_UNCHANGED__";
@@ -5,6 +5,7 @@ import { z } from "zod";
import { Button, ModalClose } from "@app/components/v2"; import { Button, ModalClose } from "@app/components/v2";
import { PamResourceType, TPostgresAccount, useGetPamResourceById } from "@app/hooks/api/pam"; import { PamResourceType, TPostgresAccount, useGetPamResourceById } from "@app/hooks/api/pam";
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas"; import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
import { SqlAccountFields } from "./shared/SqlAccountFields"; import { SqlAccountFields } from "./shared/SqlAccountFields";
@@ -34,7 +35,7 @@ export const PostgresAccountForm = ({ account, resourceId, resourceType, onSubmi
...account, ...account,
credentials: { credentials: {
...account.credentials, ...account.credentials,
password: "__INFISICAL_UNCHANGED__" password: UNCHANGED_PASSWORD_SENTINEL
} }
} }
: undefined : undefined
@@ -1,11 +1,19 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext, useWatch } from "react-hook-form";
import { FormControl, Input } from "@app/components/v2"; import { FormControl, Input } from "@app/components/v2";
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => { export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
const { control } = useFormContext(); const { control } = useFormContext();
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const password = useWatch({ control, name: "credentials.password" });
useEffect(() => {
if (password === UNCHANGED_PASSWORD_SENTINEL) {
setShowPassword(false);
}
}, [password]);
return ( return (
<div className="flex gap-2"> <div className="flex gap-2">
@@ -38,14 +46,14 @@ export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
autoComplete="new-password" autoComplete="new-password"
onFocus={() => { onFocus={() => {
if (isUpdate && field.value === "__INFISICAL_UNCHANGED__") { if (isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL) {
field.onChange(""); field.onChange("");
} }
setShowPassword(true); setShowPassword(true);
}} }}
onBlur={() => { onBlur={() => {
if (isUpdate && field.value === "") { if (isUpdate && field.value === "") {
field.onChange("__INFISICAL_UNCHANGED__"); field.onChange(UNCHANGED_PASSWORD_SENTINEL);
} }
setShowPassword(false); setShowPassword(false);
}} }}
@@ -5,6 +5,7 @@ import { z } from "zod";
import { Button, ModalClose } from "@app/components/v2"; import { Button, ModalClose } from "@app/components/v2";
import { PamResourceType, TPostgresResource } from "@app/hooks/api/pam"; import { PamResourceType, TPostgresResource } from "@app/hooks/api/pam";
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
import { BaseSqlAccountSchema } from "@app/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas"; import { BaseSqlAccountSchema } from "@app/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas";
import { BaseSqlResourceSchema } from "./shared/sql-resource-schemas"; import { BaseSqlResourceSchema } from "./shared/sql-resource-schemas";
@@ -37,7 +38,7 @@ export const PostgresResourceForm = ({ resource, onSubmit }: Props) => {
rotationAccountCredentials: resource.rotationAccountCredentials rotationAccountCredentials: resource.rotationAccountCredentials
? { ? {
...resource.rotationAccountCredentials, ...resource.rotationAccountCredentials,
password: "__INFISICAL_UNCHANGED__" password: UNCHANGED_PASSWORD_SENTINEL
} }
: resource.rotationAccountCredentials : resource.rotationAccountCredentials
} }
@@ -1,5 +1,5 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext, useWatch } from "react-hook-form";
import { import {
Accordion, Accordion,
@@ -9,10 +9,18 @@ import {
FormControl, FormControl,
Input Input
} from "@app/components/v2"; } from "@app/components/v2";
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => { export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
const { control } = useFormContext(); const { control } = useFormContext();
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const password = useWatch({ control, name: "credentials.password" });
useEffect(() => {
if (password === UNCHANGED_PASSWORD_SENTINEL) {
setShowPassword(false);
}
}, [password]);
return ( return (
<Accordion type="single" collapsible className="w-full"> <Accordion type="single" collapsible className="w-full">
@@ -55,14 +63,14 @@ export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
autoComplete="new-password" autoComplete="new-password"
onFocus={() => { onFocus={() => {
if (isUpdate && field.value === "__INFISICAL_UNCHANGED__") { if (isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL) {
field.onChange(""); field.onChange("");
} }
setShowPassword(true); setShowPassword(true);
}} }}
onBlur={() => { onBlur={() => {
if (isUpdate && field.value === "") { if (isUpdate && field.value === "") {
field.onChange("__INFISICAL_UNCHANGED__"); field.onChange(UNCHANGED_PASSWORD_SENTINEL);
} }
setShowPassword(false); setShowPassword(false);
}} }}