mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-18 10:48:36 +00:00
266 lines
8.3 KiB
TypeScript
266 lines
8.3 KiB
TypeScript
import {
|
|
AddUserToGroupCommand,
|
|
AttachUserPolicyCommand,
|
|
CreateAccessKeyCommand,
|
|
CreateUserCommand,
|
|
DeleteAccessKeyCommand,
|
|
DeleteUserCommand,
|
|
DeleteUserPolicyCommand,
|
|
DetachUserPolicyCommand,
|
|
GetUserCommand,
|
|
IAMClient,
|
|
ListAccessKeysCommand,
|
|
ListAttachedUserPoliciesCommand,
|
|
ListGroupsForUserCommand,
|
|
ListUserPoliciesCommand,
|
|
PutUserPolicyCommand,
|
|
RemoveUserFromGroupCommand
|
|
} from "@aws-sdk/client-iam";
|
|
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
|
import { randomUUID } from "crypto";
|
|
import handlebars from "handlebars";
|
|
import { z } from "zod";
|
|
|
|
import { getConfig } from "@app/lib/config/env";
|
|
import { BadRequestError } from "@app/lib/errors";
|
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
|
|
|
import { DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
|
import { compileUsernameTemplate } from "./templateUtils";
|
|
import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
|
|
|
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
|
const randomUsername = alphaNumericNanoId(32);
|
|
if (!usernameTemplate) return randomUsername;
|
|
|
|
return compileUsernameTemplate({
|
|
usernameTemplate,
|
|
randomUsername,
|
|
identity
|
|
});
|
|
};
|
|
|
|
export const AwsIamProvider = (): TDynamicProviderFns => {
|
|
const validateProviderInputs = async (inputs: unknown) => {
|
|
const providerInputs = await DynamicSecretAwsIamSchema.parseAsync(inputs);
|
|
return providerInputs;
|
|
};
|
|
|
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>, projectId: string) => {
|
|
const appCfg = getConfig();
|
|
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
|
const stsClient = new STSClient({
|
|
region: providerInputs.region,
|
|
credentials:
|
|
appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
|
|
? {
|
|
accessKeyId: appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID,
|
|
secretAccessKey: appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
|
|
}
|
|
: undefined // if hosting on AWS
|
|
});
|
|
|
|
const command = new AssumeRoleCommand({
|
|
RoleArn: providerInputs.roleArn,
|
|
RoleSessionName: `infisical-dynamic-secret-${randomUUID()}`,
|
|
DurationSeconds: 900, // 15 mins
|
|
ExternalId: projectId
|
|
});
|
|
|
|
const assumeRes = await stsClient.send(command);
|
|
|
|
if (!assumeRes.Credentials?.AccessKeyId || !assumeRes.Credentials?.SecretAccessKey) {
|
|
throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" });
|
|
}
|
|
const client = new IAMClient({
|
|
region: providerInputs.region,
|
|
credentials: {
|
|
accessKeyId: assumeRes.Credentials?.AccessKeyId,
|
|
secretAccessKey: assumeRes.Credentials?.SecretAccessKey,
|
|
sessionToken: assumeRes.Credentials?.SessionToken
|
|
}
|
|
});
|
|
return client;
|
|
}
|
|
|
|
const client = new IAMClient({
|
|
region: providerInputs.region,
|
|
credentials: {
|
|
accessKeyId: providerInputs.accessKey,
|
|
secretAccessKey: providerInputs.secretAccessKey
|
|
}
|
|
});
|
|
|
|
return client;
|
|
};
|
|
|
|
const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => {
|
|
const providerInputs = await validateProviderInputs(inputs);
|
|
const client = await $getClient(providerInputs, projectId);
|
|
const isConnected = await client
|
|
.send(new GetUserCommand({}))
|
|
.then(() => true)
|
|
.catch((err) => {
|
|
const message = (err as Error)?.message;
|
|
if (
|
|
providerInputs.method === AwsIamAuthType.AssumeRole &&
|
|
// assume role will throw an error asking to provider username, but if so this has access in aws correctly
|
|
message.includes("Must specify userName when calling with non-User credentials")
|
|
) {
|
|
return true;
|
|
}
|
|
throw err;
|
|
});
|
|
return isConnected;
|
|
};
|
|
|
|
const create = async (data: {
|
|
inputs: unknown;
|
|
expireAt: number;
|
|
usernameTemplate?: string | null;
|
|
identity?: {
|
|
name: string;
|
|
};
|
|
metadata: { projectId: string };
|
|
}) => {
|
|
const { inputs, usernameTemplate, metadata, identity } = data;
|
|
|
|
const providerInputs = await validateProviderInputs(inputs);
|
|
const client = await $getClient(providerInputs, metadata.projectId);
|
|
|
|
const username = generateUsername(usernameTemplate, identity);
|
|
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
|
const createUserRes = await client.send(
|
|
new CreateUserCommand({
|
|
Path: awsPath,
|
|
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
|
|
Tags: [{ Key: "createdBy", Value: "infisical-dynamic-secret" }],
|
|
UserName: username
|
|
})
|
|
);
|
|
|
|
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
|
|
if (userGroups) {
|
|
await Promise.all(
|
|
userGroups
|
|
.split(",")
|
|
.filter(Boolean)
|
|
.map((group) =>
|
|
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
|
|
)
|
|
);
|
|
}
|
|
if (policyArns) {
|
|
await Promise.all(
|
|
policyArns
|
|
.split(",")
|
|
.filter(Boolean)
|
|
.map((policyArn) =>
|
|
client.send(new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn }))
|
|
)
|
|
);
|
|
}
|
|
if (policyDocument) {
|
|
await client.send(
|
|
new PutUserPolicyCommand({
|
|
UserName: createUserRes.User.UserName,
|
|
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
|
|
PolicyDocument: policyDocument
|
|
})
|
|
);
|
|
}
|
|
|
|
const createAccessKeyRes = await client.send(
|
|
new CreateAccessKeyCommand({
|
|
UserName: createUserRes.User.UserName
|
|
})
|
|
);
|
|
if (!createAccessKeyRes.AccessKey)
|
|
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
|
|
|
|
return {
|
|
entityId: username,
|
|
data: {
|
|
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
|
|
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
|
|
USERNAME: username
|
|
}
|
|
};
|
|
};
|
|
|
|
const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => {
|
|
const providerInputs = await validateProviderInputs(inputs);
|
|
const client = await $getClient(providerInputs, metadata.projectId);
|
|
|
|
const username = entityId;
|
|
|
|
// remove user from groups
|
|
const userGroups = await client.send(new ListGroupsForUserCommand({ UserName: username }));
|
|
await Promise.all(
|
|
(userGroups.Groups || []).map(({ GroupName }) =>
|
|
client.send(
|
|
new RemoveUserFromGroupCommand({
|
|
GroupName,
|
|
UserName: username
|
|
})
|
|
)
|
|
)
|
|
);
|
|
|
|
// remove user access keys
|
|
const userAccessKeys = await client.send(new ListAccessKeysCommand({ UserName: username }));
|
|
await Promise.all(
|
|
(userAccessKeys.AccessKeyMetadata || []).map(({ AccessKeyId }) =>
|
|
client.send(
|
|
new DeleteAccessKeyCommand({
|
|
AccessKeyId,
|
|
UserName: username
|
|
})
|
|
)
|
|
)
|
|
);
|
|
|
|
// remove user inline policies
|
|
const userInlinePolicies = await client.send(new ListUserPoliciesCommand({ UserName: username }));
|
|
await Promise.all(
|
|
(userInlinePolicies.PolicyNames || []).map((policyName) =>
|
|
client.send(
|
|
new DeleteUserPolicyCommand({
|
|
PolicyName: policyName,
|
|
UserName: username
|
|
})
|
|
)
|
|
)
|
|
);
|
|
|
|
// remove user attached policies
|
|
const userAttachedPolicies = await client.send(new ListAttachedUserPoliciesCommand({ UserName: username }));
|
|
await Promise.all(
|
|
(userAttachedPolicies.AttachedPolicies || []).map((policy) =>
|
|
client.send(
|
|
new DetachUserPolicyCommand({
|
|
PolicyArn: policy.PolicyArn,
|
|
UserName: username
|
|
})
|
|
)
|
|
)
|
|
);
|
|
|
|
await client.send(new DeleteUserCommand({ UserName: username }));
|
|
return { entityId: username };
|
|
};
|
|
|
|
const renew = async (_inputs: unknown, entityId: string) => {
|
|
// No renewal necessary
|
|
return { entityId };
|
|
};
|
|
|
|
return {
|
|
validateProviderInputs,
|
|
validateConnection,
|
|
create,
|
|
revoke,
|
|
renew
|
|
};
|
|
};
|