mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 09:27:50 +00:00
greptile review comment
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const UNCHANGED_PASSWORD_SENTINEL = "__INFISICAL_UNCHANGED__";
|
||||||
+2
-1
@@ -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
|
||||||
|
|||||||
+12
-4
@@ -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);
|
||||||
}}
|
}}
|
||||||
|
|||||||
+2
-1
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-4
@@ -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);
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user