Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin' into fix-integration-sync-impor…
Browse files Browse the repository at this point in the history
…t-priority
  • Loading branch information
dangtony98 committed Apr 30, 2024
2 parents bf3e934 + 306709c commit 17fa72b
Show file tree
Hide file tree
Showing 26 changed files with 1,210 additions and 131 deletions.
28 changes: 28 additions & 0 deletions backend/src/db/migrations/20240429154610_audit-log-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Knex } from "knex";

import { TableName } from "../schemas";

export async function up(knex: Knex): Promise<void> {
const doesOrgIdExist = await knex.schema.hasColumn(TableName.AuditLog, "orgId");
const doesProjectIdExist = await knex.schema.hasColumn(TableName.AuditLog, "projectId");
const doesCreatedAtExist = await knex.schema.hasColumn(TableName.AuditLog, "createdAt");
if (await knex.schema.hasTable(TableName.AuditLog)) {
await knex.schema.alterTable(TableName.AuditLog, (t) => {
if (doesProjectIdExist && doesCreatedAtExist) t.index(["projectId", "createdAt"]);
if (doesOrgIdExist && doesCreatedAtExist) t.index(["orgId", "createdAt"]);
});
}
}

export async function down(knex: Knex): Promise<void> {
const doesOrgIdExist = await knex.schema.hasColumn(TableName.AuditLog, "orgId");
const doesProjectIdExist = await knex.schema.hasColumn(TableName.AuditLog, "projectId");
const doesCreatedAtExist = await knex.schema.hasColumn(TableName.AuditLog, "createdAt");

if (await knex.schema.hasTable(TableName.AuditLog)) {
await knex.schema.alterTable(TableName.AuditLog, (t) => {
if (doesProjectIdExist && doesCreatedAtExist) t.dropIndex(["projectId", "createdAt"]);
if (doesOrgIdExist && doesCreatedAtExist) t.dropIndex(["orgId", "createdAt"]);
});
}
}
194 changes: 194 additions & 0 deletions backend/src/ee/services/dynamic-secret/providers/aws-iam.ts
Original file line number Diff line number Diff line change
@@ -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
};
};
4 changes: 3 additions & 1 deletion backend/src/ee/services/dynamic-secret/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { AwsIamProvider } from "./aws-iam";
import { CassandraProvider } from "./cassandra";
import { DynamicSecretProviders } from "./models";
import { SqlDatabaseProvider } from "./sql-database";

export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
[DynamicSecretProviders.Cassandra]: CassandraProvider()
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
[DynamicSecretProviders.AwsIam]: AwsIamProvider()
});
47 changes: 30 additions & 17 deletions backend/src/ee/services/dynamic-secret/providers/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,51 @@ export enum SqlProviders {

export const DynamicSecretSqlDBSchema = z.object({
client: z.nativeEnum(SqlProviders),
host: z.string().toLowerCase(),
host: z.string().trim().toLowerCase(),
port: z.number(),
database: z.string(),
username: z.string(),
password: z.string(),
creationStatement: z.string(),
revocationStatement: z.string(),
renewStatement: z.string().optional(),
database: z.string().trim(),
username: z.string().trim(),
password: z.string().trim(),
creationStatement: z.string().trim(),
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(),
ca: z.string().optional()
});

export const DynamicSecretCassandraSchema = z.object({
host: z.string().toLowerCase(),
host: z.string().trim().toLowerCase(),
port: z.number(),
localDataCenter: z.string().min(1),
keyspace: z.string().optional(),
username: z.string(),
password: z.string(),
creationStatement: z.string(),
revocationStatement: z.string(),
renewStatement: z.string().optional(),
localDataCenter: z.string().trim().min(1),
keyspace: z.string().trim().optional(),
username: z.string().trim(),
password: z.string().trim(),
creationStatement: z.string().trim(),
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().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 {
SqlDatabase = "sql-database",
Cassandra = "cassandra"
Cassandra = "cassandra",
AwsIam = "aws-iam"
}

export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
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 = {
Expand Down
36 changes: 24 additions & 12 deletions backend/src/services/integration-auth/integration-auth-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,20 +566,32 @@ export const integrationAuthServiceFactory = ({
}
});
const kms = new AWS.KMS();

const aliases = await kms.listAliases({}).promise();
const keys = await kms.listKeys({}).promise();
const response = keys
.Keys!.map((key) => {
const keyAlias = aliases.Aliases!.find((alias) => key.KeyId === alias.TargetKeyId);
if (!keyAlias?.AliasName?.includes("alias/aws/")) {
return { id: String(key.KeyId), alias: String(keyAlias?.AliasName || key.KeyId) };
}
return { id: "null", alias: "null" };
})
.filter((elem) => elem.id !== "null");

return [...response, { id: "null", alias: "default" }];
const keyAliases = aliases.Aliases!.filter((alias) => {
if (!alias.TargetKeyId) return false;

if (integrationAuth.integration === Integrations.AWS_PARAMETER_STORE && alias.AliasName === "alias/aws/ssm")
return true;

if (
integrationAuth.integration === Integrations.AWS_SECRET_MANAGER &&
alias.AliasName === "alias/aws/secretsmanager"
)
return true;

if (alias.AliasName?.includes("alias/aws/")) return false;
return alias.TargetKeyId;
});

const keysWithAliases = keyAliases.map((alias) => {
return {
id: alias.TargetKeyId!,
alias: alias.AliasName!
};
});

return keysWithAliases;
};

const getQoveryProjects = async ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ const syncSecretsAWSParameterStore = async ({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key].value,
KeyId: metadata.kmsKeyId ? metadata.kmsKeyId : undefined,
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }),
// Overwrite: true,
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
Expand Down Expand Up @@ -572,7 +572,6 @@ const syncSecretsAWSSecretManager = async ({
if (awsSecretManagerSecret?.SecretString) {
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString);
}

if (!isEqual(awsSecretManagerSecretObj, secKeyVal)) {
await secretsManager.send(
new UpdateSecretCommand({
Expand All @@ -587,7 +586,7 @@ const syncSecretsAWSSecretManager = async ({
new CreateSecretCommand({
Name: integration.app as string,
SecretString: JSON.stringify(secKeyVal),
KmsKeyId: metadata.kmsKeyId ? metadata.kmsKeyId : null,
...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }),
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value }))
: []
Expand Down
Loading

0 comments on commit 17fa72b

Please sign in to comment.