mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(server): dynamic secret aws iam implemented
This commit is contained in:
194
backend/src/ee/services/dynamic-secret/providers/aws-iam.ts
Normal file
194
backend/src/ee/services/dynamic-secret/providers/aws-iam.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import {
|
||||||
|
AddUserToGroupCommand,
|
||||||
|
AttachUserPolicyCommand,
|
||||||
|
CreateAccessKeyCommand,
|
||||||
|
CreateUserCommand,
|
||||||
|
DeleteAccessKeyCommand,
|
||||||
|
DeleteUserCommand,
|
||||||
|
DeleteUserPolicyCommand,
|
||||||
|
DetachUserPolicyCommand,
|
||||||
|
GetUserCommand,
|
||||||
|
IAMClient,
|
||||||
|
ListAccessKeysCommand,
|
||||||
|
ListAttachedUserPoliciesCommand,
|
||||||
|
ListGroupsForUserCommand,
|
||||||
|
ListUserPoliciesCommand,
|
||||||
|
PutUserPolicyCommand,
|
||||||
|
RemoveUserFromGroupCommand
|
||||||
|
} from "@aws-sdk/client-iam";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
const generateUsername = () => {
|
||||||
|
return alphaNumericNanoId(32);
|
||||||
|
};
|
||||||
|
|
||||||
|
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>) => {
|
||||||
|
const client = new IAMClient({
|
||||||
|
region: providerInputs.region,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: providerInputs.accessKey,
|
||||||
|
secretAccessKey: providerInputs.secretAccessKey
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConnection = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const isConnected = await client.send(new GetUserCommand({})).then(() => true);
|
||||||
|
return isConnected;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const username = generateUsername();
|
||||||
|
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) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
// do nothing
|
||||||
|
const username = entityId;
|
||||||
|
return { entityId: username };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { AwsIamProvider } from "./aws-iam";
|
||||||
import { CassandraProvider } from "./cassandra";
|
import { CassandraProvider } from "./cassandra";
|
||||||
import { DynamicSecretProviders } from "./models";
|
import { DynamicSecretProviders } from "./models";
|
||||||
import { SqlDatabaseProvider } from "./sql-database";
|
import { SqlDatabaseProvider } from "./sql-database";
|
||||||
|
|
||||||
export const buildDynamicSecretProviders = () => ({
|
export const buildDynamicSecretProviders = () => ({
|
||||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
||||||
[DynamicSecretProviders.Cassandra]: CassandraProvider()
|
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||||
|
[DynamicSecretProviders.AwsIam]: AwsIamProvider()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,38 +8,51 @@ export enum SqlProviders {
|
|||||||
|
|
||||||
export const DynamicSecretSqlDBSchema = z.object({
|
export const DynamicSecretSqlDBSchema = z.object({
|
||||||
client: z.nativeEnum(SqlProviders),
|
client: z.nativeEnum(SqlProviders),
|
||||||
host: z.string().toLowerCase(),
|
host: z.string().trim().toLowerCase(),
|
||||||
port: z.number(),
|
port: z.number(),
|
||||||
database: z.string(),
|
database: z.string().trim(),
|
||||||
username: z.string(),
|
username: z.string().trim(),
|
||||||
password: z.string(),
|
password: z.string().trim(),
|
||||||
creationStatement: z.string(),
|
creationStatement: z.string().trim(),
|
||||||
revocationStatement: z.string(),
|
revocationStatement: z.string().trim(),
|
||||||
renewStatement: z.string().optional(),
|
renewStatement: z.string().trim().optional(),
|
||||||
ca: z.string().optional()
|
ca: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DynamicSecretCassandraSchema = z.object({
|
export const DynamicSecretCassandraSchema = z.object({
|
||||||
host: z.string().toLowerCase(),
|
host: z.string().trim().toLowerCase(),
|
||||||
port: z.number(),
|
port: z.number(),
|
||||||
localDataCenter: z.string().min(1),
|
localDataCenter: z.string().trim().min(1),
|
||||||
keyspace: z.string().optional(),
|
keyspace: z.string().trim().optional(),
|
||||||
username: z.string(),
|
username: z.string().trim(),
|
||||||
password: z.string(),
|
password: z.string().trim(),
|
||||||
creationStatement: z.string(),
|
creationStatement: z.string().trim(),
|
||||||
revocationStatement: z.string(),
|
revocationStatement: z.string().trim(),
|
||||||
renewStatement: z.string().optional(),
|
renewStatement: z.string().trim().optional(),
|
||||||
ca: z.string().optional()
|
ca: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const DynamicSecretAwsIamSchema = z.object({
|
||||||
|
accessKey: z.string().trim().min(1),
|
||||||
|
secretAccessKey: z.string().trim().min(1),
|
||||||
|
region: z.string().trim().min(1),
|
||||||
|
awsPath: z.string().trim().optional(),
|
||||||
|
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
||||||
|
policyDocument: z.string().trim().optional(),
|
||||||
|
userGroups: z.string().trim().optional(),
|
||||||
|
policyArns: z.string().trim().optional()
|
||||||
|
});
|
||||||
|
|
||||||
export enum DynamicSecretProviders {
|
export enum DynamicSecretProviders {
|
||||||
SqlDatabase = "sql-database",
|
SqlDatabase = "sql-database",
|
||||||
Cassandra = "cassandra"
|
Cassandra = "cassandra",
|
||||||
|
AwsIam = "aws-iam"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema })
|
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
|
||||||
|
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type TDynamicProviderFns = {
|
export type TDynamicProviderFns = {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
membersUsed: 0,
|
membersUsed: 0,
|
||||||
environmentLimit: null,
|
environmentLimit: null,
|
||||||
environmentsUsed: 0,
|
environmentsUsed: 0,
|
||||||
dynamicSecret: false,
|
dynamicSecret: true,
|
||||||
secretVersioning: true,
|
secretVersioning: true,
|
||||||
pitRecovery: false,
|
pitRecovery: false,
|
||||||
ipAllowlisting: false,
|
ipAllowlisting: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user