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

View File

@@ -3,18 +3,21 @@ import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (
!(await knex.schema.hasColumn(TableName.PamAccount, "rotationEnabled")) &&
!(await knex.schema.hasColumn(TableName.PamAccount, "rotationIntervalSeconds")) &&
!(await knex.schema.hasColumn(TableName.PamAccount, "lastRotatedAt"))
) {
if (!(await knex.schema.hasColumn(TableName.PamAccount, "rotationEnabled"))) {
await knex.schema.alterTable(TableName.PamAccount, (t) => {
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();
});
}
if (!(await knex.schema.hasColumn(TableName.PamAccount, "lastRotatedAt"))) {
await knex.schema.alterTable(TableName.PamAccount, (t) => {
t.timestamp("lastRotatedAt").nullable();
});
}
if (!(await knex.schema.hasColumn(TableName.PamResource, "encryptedRotationAccountCredentials"))) {
await knex.schema.alterTable(TableName.PamResource, (t) => {
t.binary("encryptedRotationAccountCredentials").nullable();
@@ -28,16 +31,19 @@ export async function down(knex: Knex): Promise<void> {
t.dropColumn("encryptedRotationAccountCredentials");
});
}
if (
(await knex.schema.hasColumn(TableName.PamAccount, "rotationEnabled")) &&
(await knex.schema.hasColumn(TableName.PamAccount, "rotationIntervalSeconds")) &&
(await knex.schema.hasColumn(TableName.PamAccount, "lastRotatedAt"))
) {
if (await knex.schema.hasColumn(TableName.PamAccount, "rotationEnabled")) {
await knex.schema.alterTable(TableName.PamAccount, (t) => {
t.dropColumn("lastRotatedAt");
t.dropColumn("rotationIntervalSeconds");
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");
});
}
}

View File

@@ -50,6 +50,7 @@ export const pamAccountDALFactory = (db: TDbClient) => {
.innerJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`)
.whereNotNull(`${TableName.PamResource}.encryptedRotationAccountCredentials`)
.whereNotNull(`${TableName.PamAccount}.rotationIntervalSeconds`)
.where(`${TableName.PamAccount}.rotationEnabled`, true)
.whereRaw(
`COALESCE("${TableName.PamAccount}"."lastRotatedAt", "${TableName.PamAccount}"."createdAt") + "${TableName.PamAccount}"."rotationIntervalSeconds" * interval '1 second' < NOW()`
)

View File

@@ -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);
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) {
const batch = accounts.slice(i, i + ROTATION_CONCURRENCY_LIMIT);
const rotationPromises = batch.map(async (account) => {
let logResourceType = "unknown";
try {
const resource = await pamResourceDAL.findById(account.resourceId);
if (!resource || !resource.encryptedRotationAccountCredentials) return;
logResourceType = resource.resourceType;
const rotationPromises = batch.map(async (account) =>
pamAccountDAL.transaction(async (tx) => {
let logResourceType = "unknown";
try {
const resource = await pamResourceDAL.findById(account.resourceId, tx);
if (!resource || !resource.encryptedRotationAccountCredentials) return;
logResourceType = resource.resourceType;
const { connectionDetails, rotationAccountCredentials, gatewayId, resourceType } = await decryptResource(
resource,
account.projectId,
kmsService
);
const { connectionDetails, rotationAccountCredentials, gatewayId, resourceType } = await decryptResource(
resource,
account.projectId,
kmsService
);
if (!rotationAccountCredentials) return;
if (!rotationAccountCredentials) return;
const accountCredentials = await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials,
projectId: account.projectId,
kmsService
});
const accountCredentials = await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials,
projectId: account.projectId,
kmsService
});
const factory = PAM_RESOURCE_FACTORY_MAP[resourceType as PamResource](
resourceType as PamResource,
connectionDetails,
gatewayId,
gatewayV2Service
);
const factory = PAM_RESOURCE_FACTORY_MAP[resourceType as PamResource](
resourceType as PamResource,
connectionDetails,
gatewayId,
gatewayV2Service
);
const newCredentials = await factory.rotateAccountCredentials(rotationAccountCredentials, accountCredentials);
const newCredentials = await factory.rotateAccountCredentials(
rotationAccountCredentials,
accountCredentials
);
const encryptedCredentials = await encryptAccountCredentials({
credentials: newCredentials,
projectId: account.projectId,
kmsService
});
const encryptedCredentials = await encryptAccountCredentials({
credentials: newCredentials,
projectId: account.projectId,
kmsService
});
await pamAccountDAL.updateById(account.id, {
encryptedCredentials,
lastRotatedAt: new Date()
});
await pamAccountDAL.updateById(
account.id,
{
encryptedCredentials,
lastRotatedAt: new Date()
},
tx
);
await auditLogService.createAuditLog({
projectId: account.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION,
metadata: {
accountId: account.id,
accountName: account.name,
resourceId: resource.id,
resourceType: logResourceType
await auditLogService.createAuditLog({
projectId: account.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION,
metadata: {
accountId: account.id,
accountName: account.name,
resourceId: resource.id,
resourceType: logResourceType
}
}
}
});
} catch (error) {
logger.error(error, `Failed to rotate credentials for account [accountId=${account.id}]`);
});
} catch (error) {
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({
projectId: account.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED,
metadata: {
accountId: account.id,
accountName: account.name,
resourceId: account.resourceId,
resourceType: logResourceType,
errorMessage
await auditLogService.createAuditLog({
projectId: account.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED,
metadata: {
accountId: account.id,
accountName: account.name,
resourceId: account.resourceId,
resourceType: logResourceType,
errorMessage
}
}
}
});
}
});
});
throw error; // Rollback transaction
}
})
);
// eslint-disable-next-line no-await-in-loop
await Promise.all(rotationPromises);

View File

View File

@@ -0,0 +1 @@
export const UNCHANGED_PASSWORD_SENTINEL = "__INFISICAL_UNCHANGED__";

View File

@@ -5,6 +5,7 @@ import { z } from "zod";
import { Button, ModalClose } from "@app/components/v2";
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 { SqlAccountFields } from "./shared/SqlAccountFields";
@@ -34,7 +35,7 @@ export const PostgresAccountForm = ({ account, resourceId, resourceType, onSubmi
...account,
credentials: {
...account.credentials,
password: "__INFISICAL_UNCHANGED__"
password: UNCHANGED_PASSWORD_SENTINEL
}
}
: undefined

View File

@@ -1,11 +1,19 @@
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useEffect, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { FormControl, Input } from "@app/components/v2";
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
const { control } = useFormContext();
const [showPassword, setShowPassword] = useState(false);
const password = useWatch({ control, name: "credentials.password" });
useEffect(() => {
if (password === UNCHANGED_PASSWORD_SENTINEL) {
setShowPassword(false);
}
}, [password]);
return (
<div className="flex gap-2">
@@ -38,14 +46,14 @@ export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
type={showPassword ? "text" : "password"}
autoComplete="new-password"
onFocus={() => {
if (isUpdate && field.value === "__INFISICAL_UNCHANGED__") {
if (isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL) {
field.onChange("");
}
setShowPassword(true);
}}
onBlur={() => {
if (isUpdate && field.value === "") {
field.onChange("__INFISICAL_UNCHANGED__");
field.onChange(UNCHANGED_PASSWORD_SENTINEL);
}
setShowPassword(false);
}}

View File

@@ -5,6 +5,7 @@ import { z } from "zod";
import { Button, ModalClose } from "@app/components/v2";
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 { BaseSqlResourceSchema } from "./shared/sql-resource-schemas";
@@ -37,7 +38,7 @@ export const PostgresResourceForm = ({ resource, onSubmit }: Props) => {
rotationAccountCredentials: resource.rotationAccountCredentials
? {
...resource.rotationAccountCredentials,
password: "__INFISICAL_UNCHANGED__"
password: UNCHANGED_PASSWORD_SENTINEL
}
: resource.rotationAccountCredentials
}

View File

@@ -1,5 +1,5 @@
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useEffect, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import {
Accordion,
@@ -9,10 +9,18 @@ import {
FormControl,
Input
} from "@app/components/v2";
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
const { control } = useFormContext();
const [showPassword, setShowPassword] = useState(false);
const password = useWatch({ control, name: "credentials.password" });
useEffect(() => {
if (password === UNCHANGED_PASSWORD_SENTINEL) {
setShowPassword(false);
}
}, [password]);
return (
<Accordion type="single" collapsible className="w-full">
@@ -55,14 +63,14 @@ export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
type={showPassword ? "text" : "password"}
autoComplete="new-password"
onFocus={() => {
if (isUpdate && field.value === "__INFISICAL_UNCHANGED__") {
if (isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL) {
field.onChange("");
}
setShowPassword(true);
}}
onBlur={() => {
if (isUpdate && field.value === "") {
field.onChange("__INFISICAL_UNCHANGED__");
field.onChange(UNCHANGED_PASSWORD_SENTINEL);
}
setShowPassword(false);
}}