diff --git a/backend/src/db/migrations/20240522193447_index-audit-logs-project-id-org-id.ts b/backend/src/db/migrations/20240522193447_index-audit-logs-project-id-org-id.ts new file mode 100644 index 000000000..7b208f010 --- /dev/null +++ b/backend/src/db/migrations/20240522193447_index-audit-logs-project-id-org-id.ts @@ -0,0 +1,26 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const doesOrgIdExist = await knex.schema.hasColumn(TableName.AuditLog, "orgId"); + const doesProjectIdExist = await knex.schema.hasColumn(TableName.AuditLog, "projectId"); + if (await knex.schema.hasTable(TableName.AuditLog)) { + await knex.schema.alterTable(TableName.AuditLog, (t) => { + if (doesProjectIdExist) t.index("projectId"); + if (doesOrgIdExist) t.index("orgId"); + }); + } +} + +export async function down(knex: Knex): Promise { + const doesOrgIdExist = await knex.schema.hasColumn(TableName.AuditLog, "orgId"); + const doesProjectIdExist = await knex.schema.hasColumn(TableName.AuditLog, "projectId"); + + if (await knex.schema.hasTable(TableName.AuditLog)) { + await knex.schema.alterTable(TableName.AuditLog, (t) => { + if (doesProjectIdExist) t.dropIndex("projectId"); + if (doesOrgIdExist) t.dropIndex("orgId"); + }); + } +} diff --git a/backend/src/db/migrations/20240522203425_index-secret-snapshot-secrets-envid.ts b/backend/src/db/migrations/20240522203425_index-secret-snapshot-secrets-envid.ts new file mode 100644 index 000000000..59fe14145 --- /dev/null +++ b/backend/src/db/migrations/20240522203425_index-secret-snapshot-secrets-envid.ts @@ -0,0 +1,22 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const doesEnvIdExist = await knex.schema.hasColumn(TableName.SnapshotSecret, "envId"); + if (await knex.schema.hasTable(TableName.SnapshotSecret)) { + await knex.schema.alterTable(TableName.SnapshotSecret, (t) => { + if (doesEnvIdExist) t.index("envId"); + }); + } +} + +export async function down(knex: Knex): Promise { + const doesEnvIdExist = await knex.schema.hasColumn(TableName.SnapshotSecret, "envId"); + + if (await knex.schema.hasTable(TableName.SnapshotSecret)) { + await knex.schema.alterTable(TableName.SnapshotSecret, (t) => { + if (doesEnvIdExist) t.dropIndex("envId"); + }); + } +} diff --git a/backend/src/db/migrations/20240522204414_index-secret-version-envId.ts b/backend/src/db/migrations/20240522204414_index-secret-version-envId.ts new file mode 100644 index 000000000..f01c0d3cc --- /dev/null +++ b/backend/src/db/migrations/20240522204414_index-secret-version-envId.ts @@ -0,0 +1,22 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const doesEnvIdExist = await knex.schema.hasColumn(TableName.SecretVersion, "envId"); + if (await knex.schema.hasTable(TableName.SecretVersion)) { + await knex.schema.alterTable(TableName.SecretVersion, (t) => { + if (doesEnvIdExist) t.index("envId"); + }); + } +} + +export async function down(knex: Knex): Promise { + const doesEnvIdExist = await knex.schema.hasColumn(TableName.SecretVersion, "envId"); + + if (await knex.schema.hasTable(TableName.SecretVersion)) { + await knex.schema.alterTable(TableName.SecretVersion, (t) => { + if (doesEnvIdExist) t.dropIndex("envId"); + }); + } +} diff --git a/backend/src/db/migrations/20240522212706_secret-snapshot-secrets-index-on-snapshotId.ts b/backend/src/db/migrations/20240522212706_secret-snapshot-secrets-index-on-snapshotId.ts new file mode 100644 index 000000000..7f200ed3e --- /dev/null +++ b/backend/src/db/migrations/20240522212706_secret-snapshot-secrets-index-on-snapshotId.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const doesSnapshotIdExist = await knex.schema.hasColumn(TableName.SnapshotSecret, "snapshotId"); + if (await knex.schema.hasTable(TableName.SnapshotSecret)) { + await knex.schema.alterTable(TableName.SnapshotSecret, (t) => { + if (doesSnapshotIdExist) t.index("snapshotId"); + }); + } +} + +export async function down(knex: Knex): Promise { + const doesSnapshotIdExist = await knex.schema.hasColumn(TableName.SnapshotSecret, "snapshotId"); + if (await knex.schema.hasTable(TableName.SnapshotSecret)) { + await knex.schema.alterTable(TableName.SnapshotSecret, (t) => { + if (doesSnapshotIdExist) t.dropIndex("snapshotId"); + }); + } +} diff --git a/backend/src/db/migrations/20240522221147_secret-snapshot-folder-index-on-snapshotId.ts b/backend/src/db/migrations/20240522221147_secret-snapshot-folder-index-on-snapshotId.ts new file mode 100644 index 000000000..ffb7c3336 --- /dev/null +++ b/backend/src/db/migrations/20240522221147_secret-snapshot-folder-index-on-snapshotId.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const doesSnapshotIdExist = await knex.schema.hasColumn(TableName.SnapshotFolder, "snapshotId"); + if (await knex.schema.hasTable(TableName.SnapshotFolder)) { + await knex.schema.alterTable(TableName.SnapshotFolder, (t) => { + if (doesSnapshotIdExist) t.index("snapshotId"); + }); + } +} + +export async function down(knex: Knex): Promise { + const doesSnapshotIdExist = await knex.schema.hasColumn(TableName.SnapshotFolder, "snapshotId"); + if (await knex.schema.hasTable(TableName.SnapshotFolder)) { + await knex.schema.alterTable(TableName.SnapshotFolder, (t) => { + if (doesSnapshotIdExist) t.dropIndex("snapshotId"); + }); + } +} diff --git a/backend/src/db/migrations/20240522225402_secrets-index-on-folder-id-user-id.ts b/backend/src/db/migrations/20240522225402_secrets-index-on-folder-id-user-id.ts new file mode 100644 index 000000000..f1225e264 --- /dev/null +++ b/backend/src/db/migrations/20240522225402_secrets-index-on-folder-id-user-id.ts @@ -0,0 +1,24 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const doesFolderIdExist = await knex.schema.hasColumn(TableName.Secret, "folderId"); + const doesUserIdExist = await knex.schema.hasColumn(TableName.Secret, "userId"); + if (await knex.schema.hasTable(TableName.Secret)) { + await knex.schema.alterTable(TableName.Secret, (t) => { + if (doesFolderIdExist && doesUserIdExist) t.index(["folderId", "userId"]); + }); + } +} + +export async function down(knex: Knex): Promise { + const doesFolderIdExist = await knex.schema.hasColumn(TableName.Secret, "folderId"); + const doesUserIdExist = await knex.schema.hasColumn(TableName.Secret, "userId"); + + if (await knex.schema.hasTable(TableName.Secret)) { + await knex.schema.alterTable(TableName.Secret, (t) => { + if (doesUserIdExist && doesFolderIdExist) t.dropIndex(["folderId", "userId"]); + }); + } +} diff --git a/backend/src/db/migrations/20240523003158_audit-log-add-expireAt-index.ts b/backend/src/db/migrations/20240523003158_audit-log-add-expireAt-index.ts new file mode 100644 index 000000000..b6dbf3e74 --- /dev/null +++ b/backend/src/db/migrations/20240523003158_audit-log-add-expireAt-index.ts @@ -0,0 +1,22 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const doesExpireAtExist = await knex.schema.hasColumn(TableName.AuditLog, "expiresAt"); + if (await knex.schema.hasTable(TableName.AuditLog)) { + await knex.schema.alterTable(TableName.AuditLog, (t) => { + if (doesExpireAtExist) t.index("expiresAt"); + }); + } +} + +export async function down(knex: Knex): Promise { + const doesExpireAtExist = await knex.schema.hasColumn(TableName.AuditLog, "expiresAt"); + + if (await knex.schema.hasTable(TableName.AuditLog)) { + await knex.schema.alterTable(TableName.AuditLog, (t) => { + if (doesExpireAtExist) t.dropIndex("expiresAt"); + }); + } +} diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 01f0e5142..6ae5a9d33 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -662,6 +662,7 @@ export const INTEGRATION = { secretPrefix: "The prefix for the saved secret. Used by GCP.", secretSuffix: "The suffix for the saved secret. Used by GCP.", initialSyncBehavoir: "Type of syncing behavoir with the integration.", + mappingBehavior: "The mapping behavior of the integration.", shouldAutoRedeploy: "Used by Render to trigger auto deploy.", secretGCPLabel: "The label for GCP secrets.", secretAWSTag: "The tags for AWS secrets.", diff --git a/backend/src/server/plugins/auth/inject-permission.ts b/backend/src/server/plugins/auth/inject-permission.ts index 084f18198..11a94657b 100644 --- a/backend/src/server/plugins/auth/inject-permission.ts +++ b/backend/src/server/plugins/auth/inject-permission.ts @@ -1,5 +1,6 @@ import fp from "fastify-plugin"; +import { logger } from "@app/lib/logger"; import { ActorType } from "@app/services/auth/auth-type"; // inject permission type needed based on auth extracted @@ -15,6 +16,10 @@ export const injectPermission = fp(async (server) => { orgId: req.auth.orgId, // if the req.auth.authMode is AuthMode.API_KEY, the orgId will be "API_KEY" authMethod: req.auth.authMethod // if the req.auth.authMode is AuthMode.API_KEY, the authMethod will be null }; + + logger.info( + `injectPermission: Injecting permissions for [permissionsForIdentity=${req.auth.userId}] [type=${ActorType.USER}]` + ); } else if (req.auth.actor === ActorType.IDENTITY) { req.permission = { type: ActorType.IDENTITY, @@ -22,6 +27,10 @@ export const injectPermission = fp(async (server) => { orgId: req.auth.orgId, authMethod: null }; + + logger.info( + `injectPermission: Injecting permissions for [permissionsForIdentity=${req.auth.identityId}] [type=${ActorType.IDENTITY}]` + ); } else if (req.auth.actor === ActorType.SERVICE) { req.permission = { type: ActorType.SERVICE, @@ -29,6 +38,10 @@ export const injectPermission = fp(async (server) => { orgId: req.auth.orgId, authMethod: null }; + + logger.info( + `injectPermission: Injecting permissions for [permissionsForIdentity=${req.auth.serviceTokenId}] [type=${ActorType.SERVICE}]` + ); } else if (req.auth.actor === ActorType.SCIM_CLIENT) { req.permission = { type: ActorType.SCIM_CLIENT, @@ -36,6 +49,10 @@ export const injectPermission = fp(async (server) => { orgId: req.auth.orgId, authMethod: null }; + + logger.info( + `injectPermission: Injecting permissions for [permissionsForIdentity=${req.auth.scimTokenId}] [type=${ActorType.SCIM_CLIENT}]` + ); } }); }); diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index 1fd92df3a..f23abc45b 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -8,6 +8,7 @@ import { writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { IntegrationMappingBehavior } from "@app/services/integration-auth/integration-list"; import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types"; export const registerIntegrationRouter = async (server: FastifyZodProvider) => { @@ -49,6 +50,10 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix), secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix), initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), + mappingBehavior: z + .nativeEnum(IntegrationMappingBehavior) + .optional() + .describe(INTEGRATION.CREATE.metadata.mappingBehavior), shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy), secretGCPLabel: z .object({ @@ -160,6 +165,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix), secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix), initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), + mappingBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.mappingBehavior), shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy), secretGCPLabel: z .object({ diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index e49cd3862..2aaf5d5f4 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -43,6 +43,11 @@ export enum IntegrationInitialSyncBehavior { PREFER_SOURCE = "prefer-source" } +export enum IntegrationMappingBehavior { + ONE_TO_ONE = "one-to-one", + MANY_TO_ONE = "many-to-one" +} + export enum IntegrationUrls { // integration oauth endpoints GCP_TOKEN_URL = "https://oauth2.googleapis.com/token", diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 1581eaef7..40d51c81a 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -30,7 +30,12 @@ import { BadRequestError } from "@app/lib/errors"; import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types"; import { TIntegrationDALFactory } from "../integration/integration-dal"; -import { IntegrationInitialSyncBehavior, Integrations, IntegrationUrls } from "./integration-list"; +import { + IntegrationInitialSyncBehavior, + IntegrationMappingBehavior, + Integrations, + IntegrationUrls +} from "./integration-list"; const getSecretKeyValuePair = (secrets: Record) => Object.keys(secrets).reduce>((prev, key) => { @@ -570,134 +575,149 @@ const syncSecretsAWSSecretManager = async ({ accessId: string | null; accessToken: string; }) => { - let secretsManager; - const secKeyVal = getSecretKeyValuePair(secrets); const metadata = z.record(z.any()).parse(integration.metadata || {}); - try { - if (!accessId) return; - secretsManager = new SecretsManagerClient({ - region: integration.region as string, - credentials: { - accessKeyId: accessId, - secretAccessKey: accessToken + if (!accessId) return; + + const secretsManager = new SecretsManagerClient({ + region: integration.region as string, + credentials: { + accessKeyId: accessId, + secretAccessKey: accessToken + } + }); + + const processAwsSecret = async ( + secretId: string, + secretValue: Record | string + ) => { + try { + const awsSecretManagerSecret = await secretsManager.send( + new GetSecretValueCommand({ + SecretId: secretId + }) + ); + + let secretToCompare; + if (awsSecretManagerSecret?.SecretString) { + if (typeof secretValue === "string") { + secretToCompare = awsSecretManagerSecret.SecretString; + } else { + secretToCompare = JSON.parse(awsSecretManagerSecret.SecretString); + } } - }); - const awsSecretManagerSecret = await secretsManager.send( - new GetSecretValueCommand({ - SecretId: integration.app as string - }) - ); + if (!isEqual(secretToCompare, secretValue)) { + await secretsManager.send( + new UpdateSecretCommand({ + SecretId: secretId, + SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue) + }) + ); + } - let awsSecretManagerSecretObj: { [key: string]: AWS.SecretsManager } = {}; + const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined; - if (awsSecretManagerSecret?.SecretString) { - awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString); - } + if (secretAWSTag && secretAWSTag.length) { + const describedSecret = await secretsManager.send( + // requires secretsmanager:DescribeSecret policy + new DescribeSecretCommand({ + SecretId: secretId + }) + ); - if (!isEqual(awsSecretManagerSecretObj, secKeyVal)) { - await secretsManager.send( - new UpdateSecretCommand({ - SecretId: integration.app as string, - SecretString: JSON.stringify(secKeyVal) - }) - ); - } + if (!describedSecret.Tags) return; - const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined; + const integrationTagObj = secretAWSTag.reduce( + (acc, item) => { + acc[item.key] = item.value; + return acc; + }, + {} as Record + ); - if (secretAWSTag && secretAWSTag.length) { - const describedSecret = await secretsManager.send( - // requires secretsmanager:DescribeSecret policy - new DescribeSecretCommand({ - SecretId: integration.app as string - }) - ); + const awsTagObj = (describedSecret.Tags || []).reduce( + (acc, item) => { + if (item.Key && item.Value) { + acc[item.Key] = item.Value; + } + return acc; + }, + {} as Record + ); - if (!describedSecret.Tags) return; + const tagsToUpdate: { Key: string; Value: string }[] = []; + const tagsToDelete: { Key: string; Value: string }[] = []; - const integrationTagObj = secretAWSTag.reduce( - (acc, item) => { - acc[item.key] = item.value; - return acc; - }, - {} as Record - ); - - const awsTagObj = (describedSecret.Tags || []).reduce( - (acc, item) => { - if (item.Key && item.Value) { - acc[item.Key] = item.Value; + describedSecret.Tags?.forEach((tag) => { + if (tag.Key && tag.Value) { + if (!(tag.Key in integrationTagObj)) { + // delete tag from AWS secret manager + tagsToDelete.push({ + Key: tag.Key, + Value: tag.Value + }); + } else if (tag.Value !== integrationTagObj[tag.Key]) { + // update tag in AWS secret manager + tagsToUpdate.push({ + Key: tag.Key, + Value: integrationTagObj[tag.Key] + }); + } } - return acc; - }, - {} as Record - ); + }); - const tagsToUpdate: { Key: string; Value: string }[] = []; - const tagsToDelete: { Key: string; Value: string }[] = []; - - describedSecret.Tags?.forEach((tag) => { - if (tag.Key && tag.Value) { - if (!(tag.Key in integrationTagObj)) { - // delete tag from AWS secret manager - tagsToDelete.push({ - Key: tag.Key, - Value: tag.Value - }); - } else if (tag.Value !== integrationTagObj[tag.Key]) { - // update tag in AWS secret manager + secretAWSTag?.forEach((tag) => { + if (!(tag.key in awsTagObj)) { + // create tag in AWS secret manager tagsToUpdate.push({ - Key: tag.Key, - Value: integrationTagObj[tag.Key] + Key: tag.key, + Value: tag.value }); } - } - }); + }); - secretAWSTag?.forEach((tag) => { - if (!(tag.key in awsTagObj)) { - // create tag in AWS secret manager - tagsToUpdate.push({ - Key: tag.key, - Value: tag.value - }); + if (tagsToUpdate.length) { + await secretsManager.send( + new TagResourceCommand({ + SecretId: secretId, + Tags: tagsToUpdate + }) + ); } - }); - if (tagsToUpdate.length) { - await secretsManager.send( - new TagResourceCommand({ - SecretId: integration.app as string, - Tags: tagsToUpdate - }) - ); + if (tagsToDelete.length) { + await secretsManager.send( + new UntagResourceCommand({ + SecretId: secretId, + TagKeys: tagsToDelete.map((tag) => tag.Key) + }) + ); + } } - - if (tagsToDelete.length) { + } catch (err) { + // case when AWS manager can't find the specified secret + if (err instanceof ResourceNotFoundException && secretsManager) { await secretsManager.send( - new UntagResourceCommand({ - SecretId: integration.app as string, - TagKeys: tagsToDelete.map((tag) => tag.Key) + new CreateSecretCommand({ + Name: secretId, + SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue), + ...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }), + Tags: metadata.secretAWSTag + ? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value })) + : [] }) ); } } - } catch (err) { - // case when AWS manager can't find the specified secret - if (err instanceof ResourceNotFoundException && secretsManager) { - await secretsManager.send( - new CreateSecretCommand({ - Name: integration.app as string, - SecretString: JSON.stringify(secKeyVal), - ...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }), - Tags: metadata.secretAWSTag - ? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value })) - : [] - }) - ); + }; + + if (metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE) { + for await (const [key, value] of Object.entries(secrets)) { + await processAwsSecret(key, value.value); } + } else { + await processAwsSecret(integration.app as string, getSecretKeyValuePair(secrets)); } }; diff --git a/backend/src/services/project-bot/project-bot-fns.ts b/backend/src/services/project-bot/project-bot-fns.ts index 3f22b8704..00604b37f 100644 --- a/backend/src/services/project-bot/project-bot-fns.ts +++ b/backend/src/services/project-bot/project-bot-fns.ts @@ -3,6 +3,7 @@ import { decryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/en import { BadRequestError } from "@app/lib/errors"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; +import { TProjectDALFactory } from "../project/project-dal"; import { TGetPrivateKeyDTO } from "./project-bot-types"; export const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) => @@ -13,11 +14,17 @@ export const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) => ciphertext: bot.encryptedPrivateKey }); -export const getBotKeyFnFactory = (projectBotDAL: TProjectBotDALFactory) => { +export const getBotKeyFnFactory = ( + projectBotDAL: TProjectBotDALFactory, + projectDAL: Pick +) => { const getBotKeyFn = async (projectId: string) => { - const bot = await projectBotDAL.findOne({ projectId }); + const project = await projectDAL.findById(projectId); + if (!project) throw new BadRequestError({ message: "Project not found during bot lookup." }); - if (!bot) throw new BadRequestError({ message: "failed to find bot key" }); + const bot = await projectBotDAL.findOne({ projectId: project.id }); + + if (!bot) throw new BadRequestError({ message: "Failed to find bot key" }); if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" }); if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey) throw new BadRequestError({ message: "Encryption key missing" }); diff --git a/backend/src/services/project-bot/project-bot-service.ts b/backend/src/services/project-bot/project-bot-service.ts index 23667ef67..ce7782a80 100644 --- a/backend/src/services/project-bot/project-bot-service.ts +++ b/backend/src/services/project-bot/project-bot-service.ts @@ -25,7 +25,7 @@ export const projectBotServiceFactory = ({ projectDAL, permissionService }: TProjectBotServiceFactoryDep) => { - const getBotKeyFn = getBotKeyFnFactory(projectBotDAL); + const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL); const getBotKey = async (projectId: string) => { return getBotKeyFn(projectId); diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index f8acc10d8..590c26ecc 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -1,3 +1,5 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; @@ -104,9 +106,9 @@ export const projectMembershipDALFactory = (db: TDbClient) => { } }; - const findProjectGhostUser = async (projectId: string) => { + const findProjectGhostUser = async (projectId: string, tx?: Knex) => { try { - const ghostUser = await db(TableName.ProjectMembership) + const ghostUser = await (tx || db)(TableName.ProjectMembership) .where({ projectId }) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .select(selectAllTableCols(TableName.Users)) diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 008dac593..f58fd7788 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -340,7 +340,7 @@ export const projectServiceFactory = ({ const deletedProject = await projectDAL.transaction(async (tx) => { const delProject = await projectDAL.deleteById(project.id, tx); - const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id).catch(() => null); + const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id, tx).catch(() => null); // Delete the org membership for the ghost user if it's found. if (projectGhostUser) { diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 6b2b50920..51ad7a6aa 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -608,7 +608,7 @@ export const createManySecretsRawFnFactory = ({ secretVersionTagDAL, folderDAL }: TCreateManySecretsRawFnFactory) => { - const getBotKeyFn = getBotKeyFnFactory(projectBotDAL); + const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL); const createManySecretsRawFn = async ({ projectId, environment, @@ -706,7 +706,7 @@ export const updateManySecretsRawFnFactory = ({ secretVersionTagDAL, folderDAL }: TUpdateManySecretsRawFnFactory) => { - const getBotKeyFn = getBotKeyFnFactory(projectBotDAL); + const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL); const updateManySecretsRawFn = async ({ projectId, environment, diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 677ba78fa..e434bd91f 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -7,7 +7,6 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; -import { logger } from "@app/lib/logger"; import { ActorType } from "../auth/auth-type"; import { TProjectDALFactory } from "../project/project-dal"; @@ -167,15 +166,11 @@ export const serviceTokenServiceFactory = ({ const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceToken.secretHash); if (!isMatch) throw new UnauthorizedError(); - // const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, { - // lastUsed: new Date() - // }); + const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, { + lastUsed: new Date() + }); - logger.info( - `fnValidateServiceToken: [serviceToken=${serviceToken.id}] [serviceTokenProjectId=${serviceToken.projectId}]` - ); - - return { ...serviceToken, lastUsed: serviceToken.lastUsed, orgId: project.orgId }; + return { ...serviceToken, lastUsed: updatedToken.lastUsed, orgId: project.orgId }; }; return { diff --git a/docs/images/integrations/aws/integrations-aws-secret-manager-create.png b/docs/images/integrations/aws/integrations-aws-secret-manager-create.png index 21f2213ef..e43cfbf9e 100644 Binary files a/docs/images/integrations/aws/integrations-aws-secret-manager-create.png and b/docs/images/integrations/aws/integrations-aws-secret-manager-create.png differ diff --git a/docs/integrations/cloud/aws-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx index db95c8308..9b3a8a2f8 100644 --- a/docs/integrations/cloud/aws-secret-manager.mdx +++ b/docs/integrations/cloud/aws-secret-manager.mdx @@ -72,6 +72,9 @@ Prerequisites: The region that you want to integrate with in AWS Secrets Manager. + + How you want the integration to map the secrets. The selected value could be either one to one or one to many. + The secret name/path in AWS into which you want to sync the secrets from Infisical. diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 9a1ee6fbf..7325dc4a3 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -64,6 +64,7 @@ export const useCreateIntegration = () => { secretSuffix?: string; initialSyncBehavior?: string; shouldAutoRedeploy?: boolean; + mappingBehavior?: string; secretAWSTag?: { key: string; value: string; diff --git a/frontend/src/hooks/api/integrations/types.ts b/frontend/src/hooks/api/integrations/types.ts index 345e41b1a..21e6bff26 100644 --- a/frontend/src/hooks/api/integrations/types.ts +++ b/frontend/src/hooks/api/integrations/types.ts @@ -36,6 +36,7 @@ export type TIntegration = { metadata?: { secretSuffix?: string; syncBehavior?: IntegrationSyncBehavior; + mappingBehavior?: IntegrationMappingBehavior; scope: string; org: string; project: string; @@ -48,3 +49,8 @@ export enum IntegrationSyncBehavior { PREFER_TARGET = "prefer-target", PREFER_SOURCE = "prefer-source" } + +export enum IntegrationMappingBehavior { + ONE_TO_ONE = "one-to-one", + MANY_TO_ONE = "many-to-one" +} diff --git a/frontend/src/pages/integrations/aws-secret-manager/create.tsx b/frontend/src/pages/integrations/aws-secret-manager/create.tsx index 07daff39c..2c04702e1 100644 --- a/frontend/src/pages/integrations/aws-secret-manager/create.tsx +++ b/frontend/src/pages/integrations/aws-secret-manager/create.tsx @@ -15,6 +15,7 @@ import queryString from "query-string"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries"; +import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types"; import { Button, @@ -70,6 +71,17 @@ const awsRegions = [ { name: "AWS GovCloud (US-West)", slug: "us-gov-west-1" } ]; +const mappingBehaviors = [ + { + label: "Many to One (All Infisical secrets will be mapped to a single AWS secret)", + value: IntegrationMappingBehavior.MANY_TO_ONE + }, + { + label: "One to One - (Each Infisical secret will be mapped to its own AWS secret)", + value: IntegrationMappingBehavior.ONE_TO_ONE + } +]; + export default function AWSSecretManagerCreateIntegrationPage() { const router = useRouter(); const { mutateAsync } = useCreateIntegration(); @@ -84,6 +96,9 @@ export default function AWSSecretManagerCreateIntegrationPage() { const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); const [secretPath, setSecretPath] = useState("/"); const [selectedAWSRegion, setSelectedAWSRegion] = useState(""); + const [selectedMappingBehavior, setSelectedMappingBehavior] = useState( + IntegrationMappingBehavior.MANY_TO_ONE + ); const [targetSecretName, setTargetSecretName] = useState(""); const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState(""); const [tagKey, setTagKey] = useState(""); @@ -116,7 +131,14 @@ export default function AWSSecretManagerCreateIntegrationPage() { const handleButtonClick = async () => { try { - if (targetSecretName.trim() === "") { + if (!selectedMappingBehavior) { + return; + } + + if ( + selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE && + targetSecretName.trim() === "" + ) { setTargetSecretName("Secret name cannot be blank"); return; } @@ -143,7 +165,8 @@ export default function AWSSecretManagerCreateIntegrationPage() { ] } : {}), - ...(kmsKeyId && { kmsKeyId }) + ...(kmsKeyId && { kmsKeyId }), + mappingBehavior: selectedMappingBehavior } }); @@ -248,19 +271,40 @@ export default function AWSSecretManagerCreateIntegrationPage() { ))} - - setTargetSecretName(e.target.value)} - /> + + + {selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE && ( + + setTargetSecretName(e.target.value)} + /> + + )} diff --git a/frontend/src/pages/login/select-organization.tsx b/frontend/src/pages/login/select-organization.tsx index de866a323..22006408e 100644 --- a/frontend/src/pages/login/select-organization.tsx +++ b/frontend/src/pages/login/select-organization.tsx @@ -121,6 +121,14 @@ export default function LoginPage() { } }, [router]); + // Case: User has no organizations. + // This can happen if the user was previously a member, but the organization was deleted or the user was removed. + useEffect(() => { + if (!organizations.isLoading && organizations.data?.length === 0) { + router.push("/org/none"); + } + }, [organizations.isLoading, organizations.data]); + if (userLoading || !user) { return ; } diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx index a6e3a45e7..267ff8580 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx @@ -21,6 +21,7 @@ import { import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useSyncIntegration } from "@app/hooks/api/integrations/queries"; +import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types"; import { TIntegration } from "@app/hooks/api/types"; type Props = { @@ -131,30 +132,35 @@ export const IntegrationsSection = ({ )} -
- -
- {(integration.integration === "hashicorp-vault" && - `${integration.app} - path: ${integration.path}`) || - (integration.scope === "github-org" && `${integration.owner}`) || - (integration.integration === "aws-parameter-store" && - `${integration.path}`) || - (integration.scope?.startsWith("github-") && - `${integration.owner}/${integration.app}`) || - integration.app} + {!( + integration.integration === "aws-secret-manager" && + integration.metadata?.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE + ) && ( +
+ +
+ {(integration.integration === "hashicorp-vault" && + `${integration.app} - path: ${integration.path}`) || + (integration.scope === "github-org" && `${integration.owner}`) || + (integration.integration === "aws-parameter-store" && + `${integration.path}`) || + (integration.scope?.startsWith("github-") && + `${integration.owner}/${integration.app}`) || + integration.app} +
-
+ )} {(integration.integration === "vercel" || integration.integration === "netlify" || integration.integration === "railway" ||