feat: enhance PAM account handling with type safety and improved response structure

- Introduced type inference for sanitized accounts to ensure consistent data handling.
- Updated account response structure to explicitly cast accounts to the sanitized type.
- Refined the decryption function to omit sensitive fields from the returned account object.
- Improved error handling in SQL resource factory by enforcing required gateway ID validation.
This commit is contained in:
Victor Santos
2025-12-05 00:56:33 -03:00
parent aac84e3952
commit ac5c185f76
9 changed files with 39 additions and 20 deletions
@@ -23,6 +23,8 @@ const SanitizedAccountSchema = z.union([
SanitizedAwsIamAccountWithResourceSchema SanitizedAwsIamAccountWithResourceSchema
]); ]);
type TSanitizedAccount = z.infer<typeof SanitizedAccountSchema>;
export const registerPamAccountRouter = async (server: FastifyZodProvider) => { export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
@@ -95,7 +97,7 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
} }
}); });
return { accounts, folders, totalCount, folderId, folderPaths }; return { accounts: accounts as TSanitizedAccount[], folders, totalCount, folderId, folderPaths };
} }
}); });
@@ -72,17 +72,24 @@ export const decryptAccount = async <
account: T, account: T,
projectId: string, projectId: string,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey"> kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
): Promise<T & { credentials: TPamAccountCredentials; lastRotationMessage: string | null }> => { ): Promise<
Omit<T, "encryptedCredentials" | "encryptedLastRotationMessage"> & {
credentials: TPamAccountCredentials;
lastRotationMessage: string | null;
}
> => {
const { encryptedCredentials, encryptedLastRotationMessage, ...rest } = account;
return { return {
...account, ...rest,
credentials: await decryptAccountCredentials({ credentials: await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials, encryptedCredentials,
projectId, projectId,
kmsService kmsService
}), }),
lastRotationMessage: account.encryptedLastRotationMessage lastRotationMessage: encryptedLastRotationMessage
? await decryptAccountMessage({ ? await decryptAccountMessage({
encryptedMessage: account.encryptedLastRotationMessage, encryptedMessage: encryptedLastRotationMessage,
projectId, projectId,
kmsService kmsService
}) })
@@ -439,7 +439,7 @@ export const pamAccountServiceFactory = ({
const totalCount = totalFolderCount + totalAccountCount; const totalCount = totalFolderCount + totalAccountCount;
const decryptedAndPermittedAccounts: Array< const decryptedAndPermittedAccounts: Array<
TPamAccounts & { Omit<TPamAccounts, "encryptedCredentials" | "encryptedLastRotationMessage"> & {
resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean }; resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean };
credentials: TPamAccountCredentials; credentials: TPamAccountCredentials;
lastRotationMessage: string | null; lastRotationMessage: string | null;
@@ -170,12 +170,6 @@ export const generateConsoleFederationUrl = async ({
const federationEndpoint = "https://signin.aws.amazon.com/federation"; const federationEndpoint = "https://signin.aws.amazon.com/federation";
// Console destination can be regional
const getConsoleHost = () =>
connectionDetails.region === "us-east-1"
? "console.aws.amazon.com"
: `${connectionDetails.region}.console.aws.amazon.com`;
const signinTokenUrl = `${federationEndpoint}?Action=getSigninToken&Session=${encodeURIComponent(sessionJson)}`; const signinTokenUrl = `${federationEndpoint}?Action=getSigninToken&Session=${encodeURIComponent(sessionJson)}`;
const tokenResponse = await fetch(signinTokenUrl); const tokenResponse = await fetch(signinTokenUrl);
@@ -199,7 +193,7 @@ export const generateConsoleFederationUrl = async ({
throw new Error(`AWS federation endpoint did not return a SigninToken: ${responseText.substring(0, 200)}`); throw new Error(`AWS federation endpoint did not return a SigninToken: ${responseText.substring(0, 200)}`);
} }
const consoleDestination = `https://${getConsoleHost()}/`; const consoleDestination = `https://console.aws.amazon.com/`;
const consoleUrl = `${federationEndpoint}?Action=login&SigninToken=${encodeURIComponent(tokenData.SigninToken)}&Destination=${encodeURIComponent(consoleDestination)}`; const consoleUrl = `${federationEndpoint}?Action=login&SigninToken=${encodeURIComponent(tokenData.SigninToken)}&Destination=${encodeURIComponent(consoleDestination)}`;
return { return {
@@ -1,3 +1,5 @@
import RE2 from "re2";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { AwsIamResourceListItemSchema } from "./aws-iam-resource-schemas"; import { AwsIamResourceListItemSchema } from "./aws-iam-resource-schemas";
@@ -14,7 +16,7 @@ export const getAwsIamResourceListItem = () => {
* ARN format: arn:aws:iam::123456789012:role/RoleName * ARN format: arn:aws:iam::123456789012:role/RoleName
*/ */
export const extractAwsAccountIdFromArn = (roleArn: string): string => { export const extractAwsAccountIdFromArn = (roleArn: string): string => {
const match = roleArn.match(/^arn:aws:iam::(\d{12}):role\//); const match = roleArn.match(new RE2("^arn:aws:iam::(\\d{12}):role/"));
if (!match) { if (!match) {
throw new BadRequestError({ message: "Invalid IAM Role ARN format" }); throw new BadRequestError({ message: "Invalid IAM Role ARN format" });
} }
@@ -67,9 +67,7 @@ export const AwsIamAccountSchema = BasePamAccountSchema.extend({
}); });
export const CreateAwsIamAccountSchema = BaseCreatePamAccountSchema.extend({ export const CreateAwsIamAccountSchema = BaseCreatePamAccountSchema.extend({
credentials: AwsIamAccountCredentialsSchema, credentials: AwsIamAccountCredentialsSchema
// AWS IAM doesn't support credential rotation - credentials are generated on-the-fly via STS
rotationEnabled: z.boolean().optional().default(false)
}); });
export const UpdateAwsIamAccountSchema = BaseUpdatePamAccountSchema.extend({ export const UpdateAwsIamAccountSchema = BaseUpdatePamAccountSchema.extend({
@@ -233,6 +233,10 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
gatewayV2Service gatewayV2Service
) => { ) => {
const validateConnection = async () => { const validateConnection = async () => {
if (!gatewayId) {
throw new BadRequestError({ message: "Gateway ID is required" });
}
try { try {
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => { await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => {
await client.validate(true); await client.validate(true);
@@ -255,6 +259,10 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
credentials credentials
) => { ) => {
try { try {
if (!gatewayId) {
throw new BadRequestError({ message: "Gateway ID is required" });
}
await executeWithGateway( await executeWithGateway(
{ {
connectionDetails, connectionDetails,
@@ -296,6 +304,10 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
currentCredentials currentCredentials
) => { ) => {
const newPassword = alphaNumericNanoId(32); const newPassword = alphaNumericNanoId(32);
if (!gatewayId) {
throw new BadRequestError({ message: "Gateway ID is required" });
}
try { try {
return await executeWithGateway( return await executeWithGateway(
{ {
@@ -73,7 +73,11 @@ const CreateForm = ({
); );
case PamResourceType.AwsIam: case PamResourceType.AwsIam:
return ( return (
<AwsIamAccountForm onSubmit={onSubmit} resourceId={resourceId} resourceType={resourceType} /> <AwsIamAccountForm
onSubmit={onSubmit}
resourceId={resourceId}
resourceType={resourceType}
/>
); );
default: default:
throw new Error(`Unhandled resource: ${resourceType}`); throw new Error(`Unhandled resource: ${resourceType}`);
@@ -103,7 +103,7 @@ export const PamAccountRow = ({
</span> </span>
</Badge> </Badge>
)} )}
{account.lastRotatedAt && ( {"lastRotatedAt" in account && account.lastRotatedAt && (
<Tooltip <Tooltip
className="max-w-sm text-center" className="max-w-sm text-center"
isDisabled={!account.lastRotationMessage} isDisabled={!account.lastRotationMessage}