mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
pam: rotation status & error improvements
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,9 @@ export const PamAccountsSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
rotationEnabled: z.boolean().default(false),
|
rotationEnabled: z.boolean().default(false),
|
||||||
rotationIntervalSeconds: z.number().nullable().optional(),
|
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<typeof PamAccountsSchema>;
|
export type TPamAccounts = z.infer<typeof PamAccountsSchema>;
|
||||||
|
|||||||
@@ -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 { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||||
import {
|
import {
|
||||||
CreatePostgresResourceSchema,
|
CreatePostgresResourceSchema,
|
||||||
SanitizedPostgresResourceSchema,
|
SanitizedPostgresResourceSchema,
|
||||||
UpdatePostgresResourceSchema
|
UpdatePostgresResourceSchema
|
||||||
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
} 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";
|
import { registerPamResourceEndpoints } from "./pam-resource-endpoints";
|
||||||
|
|
||||||
|
|||||||
@@ -45,17 +45,47 @@ export const decryptAccountCredentials = async ({
|
|||||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials;
|
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const decryptAccount = async <T extends { encryptedCredentials: Buffer }>(
|
export const decryptAccountMessage = async ({
|
||||||
|
projectId,
|
||||||
|
encryptedMessage,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
encryptedMessage: Buffer;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
}) => {
|
||||||
|
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,
|
account: T,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
): Promise<T & { credentials: TPamAccountCredentials }> => {
|
): Promise<T & { credentials: TPamAccountCredentials; lastRotationMessage: string | null }> => {
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
credentials: await decryptAccountCredentials({
|
credentials: await decryptAccountCredentials({
|
||||||
encryptedCredentials: account.encryptedCredentials,
|
encryptedCredentials: account.encryptedCredentials,
|
||||||
projectId,
|
projectId,
|
||||||
kmsService
|
kmsService
|
||||||
})
|
}),
|
||||||
} as T & { credentials: TPamAccountCredentials };
|
lastRotationMessage: account.encryptedLastRotationMessage
|
||||||
|
? await decryptAccountMessage({
|
||||||
|
encryptedMessage: account.encryptedLastRotationMessage,
|
||||||
|
projectId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
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 { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
@@ -353,6 +354,7 @@ export const pamAccountServiceFactory = ({
|
|||||||
TPamAccounts & {
|
TPamAccounts & {
|
||||||
resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean };
|
resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean };
|
||||||
credentials: TPamAccountCredentials;
|
credentials: TPamAccountCredentials;
|
||||||
|
lastRotationMessage: string | null;
|
||||||
}
|
}
|
||||||
> = [];
|
> = [];
|
||||||
|
|
||||||
@@ -376,6 +378,7 @@ export const pamAccountServiceFactory = ({
|
|||||||
) {
|
) {
|
||||||
// Decrypt the account only if the user has permission to read it
|
// Decrypt the account only if the user has permission to read it
|
||||||
const decryptedAccount = await decryptAccount(account, account.projectId, kmsService);
|
const decryptedAccount = await decryptAccount(account, account.projectId, kmsService);
|
||||||
|
|
||||||
decryptedAndPermittedAccounts.push({
|
decryptedAndPermittedAccounts.push({
|
||||||
...decryptedAccount,
|
...decryptedAccount,
|
||||||
resource: {
|
resource: {
|
||||||
@@ -619,7 +622,9 @@ export const pamAccountServiceFactory = ({
|
|||||||
account.id,
|
account.id,
|
||||||
{
|
{
|
||||||
encryptedCredentials,
|
encryptedCredentials,
|
||||||
lastRotatedAt: new Date()
|
lastRotatedAt: new Date(),
|
||||||
|
rotationStatus: "success",
|
||||||
|
encryptedLastRotationMessage: null
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -645,6 +650,24 @@ export const pamAccountServiceFactory = ({
|
|||||||
|
|
||||||
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
|
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({
|
await auditLogService.createAuditLog({
|
||||||
projectId: account.projectId,
|
projectId: account.projectId,
|
||||||
actor: {
|
actor: {
|
||||||
@@ -662,7 +685,6 @@ export const pamAccountServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
throw error; // Rollback transaction
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ export const BasePamAccountSchemaWithResource = BasePamAccountSchema.extend({
|
|||||||
resourceType: true
|
resourceType: true
|
||||||
}).extend({
|
}).extend({
|
||||||
rotationCredentialsConfigured: z.boolean()
|
rotationCredentialsConfigured: z.boolean()
|
||||||
})
|
}),
|
||||||
|
lastRotationMessage: z.string().nullable().optional(),
|
||||||
|
rotationStatus: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const BaseCreatePamAccountSchema = z.object({
|
export const BaseCreatePamAccountSchema = z.object({
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export interface TBasePamAccount {
|
|||||||
rotationEnabled: boolean;
|
rotationEnabled: boolean;
|
||||||
rotationIntervalSeconds?: number | null;
|
rotationIntervalSeconds?: number | null;
|
||||||
lastRotatedAt?: string | null;
|
lastRotatedAt?: string | null;
|
||||||
|
lastRotationMessage?: string | null;
|
||||||
|
rotationStatus?: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { PamResourceType, PamSessionStatus } from "../enums";
|
import { PamResourceType, PamSessionStatus } from "../enums";
|
||||||
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
|
|
||||||
import { TMySQLAccount, TMySQLResource } from "./mysql-resource";
|
import { TMySQLAccount, TMySQLResource } from "./mysql-resource";
|
||||||
|
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
|
||||||
|
|
||||||
export * from "./postgres-resource";
|
|
||||||
export * from "./mysql-resource";
|
export * from "./mysql-resource";
|
||||||
|
export * from "./postgres-resource";
|
||||||
|
|
||||||
export type TPamResource = TPostgresResource | TMySQLResource;
|
export type TPamResource = TPostgresResource | TMySQLResource;
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Button, ModalClose } from "@app/components/v2";
|
import { Button, ModalClose } from "@app/components/v2";
|
||||||
import { PamResourceType, TMySQLAccount } from "@app/hooks/api/pam";
|
import { PamResourceType, TMySQLAccount } from "@app/hooks/api/pam";
|
||||||
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
||||||
|
|
||||||
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
|
||||||
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
|
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
|
||||||
import { SqlAccountFields } from "./shared/SqlAccountFields";
|
import { SqlAccountFields } from "./shared/SqlAccountFields";
|
||||||
|
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
account?: TMySQLAccount;
|
account?: TMySQLAccount;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Button, ModalClose } from "@app/components/v2";
|
import { Button, ModalClose } from "@app/components/v2";
|
||||||
@@ -12,10 +12,10 @@ import {
|
|||||||
} from "@app/hooks/api/pam";
|
} from "@app/hooks/api/pam";
|
||||||
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
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 { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
|
||||||
import { SqlAccountFields } from "./shared/SqlAccountFields";
|
import { SqlAccountFields } from "./shared/SqlAccountFields";
|
||||||
|
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
||||||
|
import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
account?: TPostgresAccount;
|
account?: TPostgresAccount;
|
||||||
|
|||||||
@@ -103,10 +103,23 @@ export const PamAccountRow = ({
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{account.lastRotatedAt && (
|
{account.lastRotatedAt && (
|
||||||
<Badge className="flex h-5 w-min items-center gap-1.5 bg-orange/20 whitespace-nowrap text-orange">
|
<Tooltip
|
||||||
<FontAwesomeIcon icon={faRotate} />
|
className="max-w-sm text-center"
|
||||||
<span>Rotated {formatDistance(new Date(), account.lastRotatedAt)} ago</span>
|
isDisabled={!account.lastRotationMessage}
|
||||||
</Badge>
|
content={account.lastRotationMessage}
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
className={twMerge(
|
||||||
|
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap",
|
||||||
|
account.rotationStatus === "failed"
|
||||||
|
? "bg-red/20 text-red"
|
||||||
|
: "bg-green/20 text-green"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faRotate} />
|
||||||
|
<span>Rotated {formatDistance(new Date(), account.lastRotatedAt)} ago</span>
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user