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

View File

@@ -23,6 +23,8 @@ const SanitizedAccountSchema = z.union([
SanitizedAwsIamAccountWithResourceSchema
]);
type TSanitizedAccount = z.infer<typeof SanitizedAccountSchema>;
export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
server.route({
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 };
}
});

View File

@@ -72,17 +72,24 @@ export const decryptAccount = async <
account: T,
projectId: string,
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 {
...account,
...rest,
credentials: await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials,
encryptedCredentials,
projectId,
kmsService
}),
lastRotationMessage: account.encryptedLastRotationMessage
lastRotationMessage: encryptedLastRotationMessage
? await decryptAccountMessage({
encryptedMessage: account.encryptedLastRotationMessage,
encryptedMessage: encryptedLastRotationMessage,
projectId,
kmsService
})

View File

@@ -439,7 +439,7 @@ export const pamAccountServiceFactory = ({
const totalCount = totalFolderCount + totalAccountCount;
const decryptedAndPermittedAccounts: Array<
TPamAccounts & {
Omit<TPamAccounts, "encryptedCredentials" | "encryptedLastRotationMessage"> & {
resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean };
credentials: TPamAccountCredentials;
lastRotationMessage: string | null;

View File

@@ -170,12 +170,6 @@ export const generateConsoleFederationUrl = async ({
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 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)}`);
}
const consoleDestination = `https://${getConsoleHost()}/`;
const consoleDestination = `https://console.aws.amazon.com/`;
const consoleUrl = `${federationEndpoint}?Action=login&SigninToken=${encodeURIComponent(tokenData.SigninToken)}&Destination=${encodeURIComponent(consoleDestination)}`;
return {

View File

@@ -1,3 +1,5 @@
import RE2 from "re2";
import { BadRequestError } from "@app/lib/errors";
import { AwsIamResourceListItemSchema } from "./aws-iam-resource-schemas";
@@ -14,7 +16,7 @@ export const getAwsIamResourceListItem = () => {
* ARN format: arn:aws:iam::123456789012:role/RoleName
*/
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) {
throw new BadRequestError({ message: "Invalid IAM Role ARN format" });
}

View File

@@ -67,9 +67,7 @@ export const AwsIamAccountSchema = BasePamAccountSchema.extend({
});
export const CreateAwsIamAccountSchema = BaseCreatePamAccountSchema.extend({
credentials: AwsIamAccountCredentialsSchema,
// AWS IAM doesn't support credential rotation - credentials are generated on-the-fly via STS
rotationEnabled: z.boolean().optional().default(false)
credentials: AwsIamAccountCredentialsSchema
});
export const UpdateAwsIamAccountSchema = BaseUpdatePamAccountSchema.extend({

View File

@@ -233,6 +233,10 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
gatewayV2Service
) => {
const validateConnection = async () => {
if (!gatewayId) {
throw new BadRequestError({ message: "Gateway ID is required" });
}
try {
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => {
await client.validate(true);
@@ -255,6 +259,10 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
credentials
) => {
try {
if (!gatewayId) {
throw new BadRequestError({ message: "Gateway ID is required" });
}
await executeWithGateway(
{
connectionDetails,
@@ -296,6 +304,10 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
currentCredentials
) => {
const newPassword = alphaNumericNanoId(32);
if (!gatewayId) {
throw new BadRequestError({ message: "Gateway ID is required" });
}
try {
return await executeWithGateway(
{

View File

@@ -73,7 +73,11 @@ const CreateForm = ({
);
case PamResourceType.AwsIam:
return (
<AwsIamAccountForm onSubmit={onSubmit} resourceId={resourceId} resourceType={resourceType} />
<AwsIamAccountForm
onSubmit={onSubmit}
resourceId={resourceId}
resourceType={resourceType}
/>
);
default:
throw new Error(`Unhandled resource: ${resourceType}`);

View File

@@ -103,7 +103,7 @@ export const PamAccountRow = ({
</span>
</Badge>
)}
{account.lastRotatedAt && (
{"lastRotatedAt" in account && account.lastRotatedAt && (
<Tooltip
className="max-w-sm text-center"
isDisabled={!account.lastRotationMessage}