From 8646f6c50bdeda666a716acbce0c43f50dd16f6e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 28 Aug 2024 17:38:16 +0400 Subject: [PATCH] Requested changes --- .../providers/aws-elasticache.ts | 129 +++++++++++++++++- backend/src/lib/aws/elasticache.ts | 125 ----------------- backend/src/lib/aws/index.ts | 1 - 3 files changed, 125 insertions(+), 130 deletions(-) delete mode 100644 backend/src/lib/aws/elasticache.ts delete mode 100644 backend/src/lib/aws/index.ts diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts index 78eacbb03..2cb862029 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts @@ -1,11 +1,131 @@ +import { + CreateUserCommand, + CreateUserGroupCommand, + DeleteUserCommand, + DescribeReplicationGroupsCommand, + DescribeUserGroupsCommand, + ElastiCache, + ModifyReplicationGroupCommand, + ModifyUserGroupCommand +} from "@aws-sdk/client-elasticache"; import handlebars from "handlebars"; import { customAlphabet } from "nanoid"; +import { z } from "zod"; -import { CreateElastiCacheUserSchema, DeleteElasticCacheUserSchema, ElastiCacheUserManager } from "@app/lib/aws"; import { BadRequestError } from "@app/lib/errors"; import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models"; +const CreateElastiCacheUserSchema = z.object({ + UserId: z.string().trim().min(1), + UserName: z.string().trim().min(1), + Engine: z.string().default("redis"), + Passwords: z.array(z.string().trim().min(1)).min(1).max(1), // Minimum password length is 16 characters, required by AWS. + AccessString: z.string().trim().min(1) // Example: "on ~* +@all" +}); + +const DeleteElasticCacheUserSchema = z.object({ + UserId: z.string().trim().min(1) +}); + +type TElastiCacheRedisUser = { userId: string; password: string }; +type TBasicAWSCredentials = { accessKeyId: string; secretAccessKey: string }; + +type TCreateElastiCacheUserInput = z.infer; +type TDeleteElastiCacheUserInput = z.infer; + +const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: string) => { + const elastiCache = new ElastiCache({ + region, + credentials + }); + const infisicalGroup = "infisical-managed-group-elasticache"; + + const ensureInfisicalGroupExists = async (clusterName: string) => { + const replicationGroups = await elastiCache.send(new DescribeUserGroupsCommand()); + + const existingGroup = replicationGroups.UserGroups?.find((group) => group.UserGroupId === infisicalGroup); + + let newlyCreatedGroup = false; + if (!existingGroup) { + const createGroupCommand = new CreateUserGroupCommand({ + UserGroupId: infisicalGroup, + UserIds: ["default"], + Engine: "redis" + }); + + await elastiCache.send(createGroupCommand); + newlyCreatedGroup = true; + } + + if (existingGroup || newlyCreatedGroup) { + const replicationGroup = ( + await elastiCache.send( + new DescribeReplicationGroupsCommand({ + ReplicationGroupId: clusterName + }) + ) + ).ReplicationGroups?.[0]; + + if (!replicationGroup?.UserGroupIds?.includes(infisicalGroup)) { + // If the replication group doesn't have the infisical user group, we need to associate it + const modifyGroupCommand = new ModifyReplicationGroupCommand({ + UserGroupIdsToAdd: [infisicalGroup], + UserGroupIdsToRemove: [], + ApplyImmediately: true, + ReplicationGroupId: clusterName + }); + await elastiCache.send(modifyGroupCommand); + } + } + }; + + const addUserToInfisicalGroup = async (userId: string) => { + // figure out if the default user is already in the group, if it is, then we shouldn't add it again + + const addUserToGroupCommand = new ModifyUserGroupCommand({ + UserGroupId: infisicalGroup, + UserIdsToAdd: [userId], + UserIdsToRemove: [] + }); + + await elastiCache.send(addUserToGroupCommand); + }; + + const createUser = async (creationInput: TCreateElastiCacheUserInput, clusterName: string) => { + await ensureInfisicalGroupExists(clusterName); + + await elastiCache.send(new CreateUserCommand(creationInput)); // First create the user + await addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists() + + return { + userId: creationInput.UserId, + password: creationInput.Passwords[0] + }; + }; + + const deleteUser = async ( + deletionInput: TDeleteElastiCacheUserInput + ): Promise> => { + await elastiCache.send(new DeleteUserCommand(deletionInput)); + return { userId: deletionInput.UserId }; + }; + + const verifyCredentials = async (clusterName: string) => { + await elastiCache.send( + new DescribeReplicationGroupsCommand({ + ReplicationGroupId: clusterName + }) + ); + }; + + return { + createUser, + deleteUser, + verifyCredentials + }; +}; + const generatePassword = () => { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; return customAlphabet(charset, 64)(); @@ -20,12 +140,13 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = DynamicSecretAwsElastiCacheSchema.parse(inputs); - JSON.parse(providerInputs.creationStatement); - JSON.parse(providerInputs.revocationStatement); + // We need to ensure the that the creation & revocation statements are valid and can be used to create and revoke users. + // We can't return the parsed statements here because we need to use the handlebars template to generate the username and password, before we can use the parsed statements. + CreateElastiCacheUserSchema.parse(JSON.parse(providerInputs.creationStatement)); + DeleteElasticCacheUserSchema.parse(JSON.parse(providerInputs.revocationStatement)); return providerInputs; }; - const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); await ElastiCacheUserManager( diff --git a/backend/src/lib/aws/elasticache.ts b/backend/src/lib/aws/elasticache.ts deleted file mode 100644 index bf963d5e6..000000000 --- a/backend/src/lib/aws/elasticache.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { - CreateUserCommand, - CreateUserGroupCommand, - DeleteUserCommand, - DescribeReplicationGroupsCommand, - DescribeUserGroupsCommand, - ElastiCache, - ModifyReplicationGroupCommand, - ModifyUserGroupCommand -} from "@aws-sdk/client-elasticache"; -import { z } from "zod"; - -type TElastiCacheRedisUser = { - userId: string; - password: string; -}; - -type TBasicAWSCredentials = { accessKeyId: string; secretAccessKey: string }; - -export const CreateElastiCacheUserSchema = z.object({ - UserId: z.string().trim().min(1), - UserName: z.string().trim().min(1), - Engine: z.string().default("redis"), - Passwords: z.array(z.string().trim().min(1)).min(1).max(1), // Minimum password length is 16 characters, required by AWS. - AccessString: z.string().trim().min(1) // Example: "on ~* +@all" -}); - -export const DeleteElasticCacheUserSchema = z.object({ - UserId: z.string().trim().min(1) -}); - -export type TCreateElastiCacheUserInput = z.infer; -export type TDeleteElastiCacheUserInput = z.infer; - -export const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: string) => { - const elastiCache = new ElastiCache({ - region, - credentials - }); - const infisicalGroup = "infisical-managed-group-elasticache"; - - const ensureInfisicalGroupExists = async (clusterName: string) => { - const replicationGroups = await elastiCache.send(new DescribeUserGroupsCommand()); - - const existingGroup = replicationGroups.UserGroups?.find((group) => group.UserGroupId === infisicalGroup); - - let newlyCreatedGroup = false; - if (!existingGroup) { - const createGroupCommand = new CreateUserGroupCommand({ - UserGroupId: infisicalGroup, - UserIds: ["default"], - Engine: "redis" - }); - - await elastiCache.send(createGroupCommand); - newlyCreatedGroup = true; - } - - if (existingGroup || newlyCreatedGroup) { - const replicationGroup = ( - await elastiCache.send( - new DescribeReplicationGroupsCommand({ - ReplicationGroupId: clusterName - }) - ) - ).ReplicationGroups?.[0]; - - if (!replicationGroup?.UserGroupIds?.includes(infisicalGroup)) { - // If the replication group doesn't have the infisical user group, we need to associate it - const modifyGroupCommand = new ModifyReplicationGroupCommand({ - UserGroupIdsToAdd: [infisicalGroup], - UserGroupIdsToRemove: [], - ApplyImmediately: true, - ReplicationGroupId: clusterName - }); - await elastiCache.send(modifyGroupCommand); - } - } - }; - - const addUserToInfisicalGroup = async (userId: string) => { - // figure out if the default user is already in the group, if it is, then we shouldn't add it again - - const addUserToGroupCommand = new ModifyUserGroupCommand({ - UserGroupId: infisicalGroup, - UserIdsToAdd: [userId], - UserIdsToRemove: [] - }); - - await elastiCache.send(addUserToGroupCommand); - }; - - const createUser = async (creationInput: TCreateElastiCacheUserInput, clusterName: string) => { - await ensureInfisicalGroupExists(clusterName); - - await elastiCache.send(new CreateUserCommand(creationInput)); // First create the user - await addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists() - - return { - userId: creationInput.UserId, - password: creationInput.Passwords[0] - }; - }; - - const deleteUser = async ( - deletionInput: TDeleteElastiCacheUserInput - ): Promise> => { - await elastiCache.send(new DeleteUserCommand(deletionInput)); - return { userId: deletionInput.UserId }; - }; - - const verifyCredentials = async (clusterName: string) => { - await elastiCache.send( - new DescribeReplicationGroupsCommand({ - ReplicationGroupId: clusterName - }) - ); - }; - - return { - createUser, - deleteUser, - verifyCredentials - }; -}; diff --git a/backend/src/lib/aws/index.ts b/backend/src/lib/aws/index.ts deleted file mode 100644 index 4abf62570..000000000 --- a/backend/src/lib/aws/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./elasticache";