Merge branch 'main' into feat/secure-mfa-endpoints-with-improved-rate-limiting

This commit is contained in:
Sheen Capadngan
2024-05-24 22:18:05 +08:00
25 changed files with 437 additions and 158 deletions
@@ -0,0 +1,26 @@
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");
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<void> {
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");
});
}
}
@@ -0,0 +1,22 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
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");
});
}
}
@@ -0,0 +1,22 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
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");
});
}
}
@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
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");
});
}
}
@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
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");
});
}
}
@@ -0,0 +1,24 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
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"]);
});
}
}
@@ -0,0 +1,22 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
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");
});
}
}
+1
View File
@@ -662,6 +662,7 @@ export const INTEGRATION = {
secretPrefix: "The prefix for the saved secret. Used by GCP.", secretPrefix: "The prefix for the saved secret. Used by GCP.",
secretSuffix: "The suffix 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.", initialSyncBehavoir: "Type of syncing behavoir with the integration.",
mappingBehavior: "The mapping behavior of the integration.",
shouldAutoRedeploy: "Used by Render to trigger auto deploy.", shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
secretGCPLabel: "The label for GCP secrets.", secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tags for AWS secrets.", secretAWSTag: "The tags for AWS secrets.",
@@ -1,5 +1,6 @@
import fp from "fastify-plugin"; import fp from "fastify-plugin";
import { logger } from "@app/lib/logger";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
// inject permission type needed based on auth extracted // 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" 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 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) { } else if (req.auth.actor === ActorType.IDENTITY) {
req.permission = { req.permission = {
type: ActorType.IDENTITY, type: ActorType.IDENTITY,
@@ -22,6 +27,10 @@ export const injectPermission = fp(async (server) => {
orgId: req.auth.orgId, orgId: req.auth.orgId,
authMethod: null authMethod: null
}; };
logger.info(
`injectPermission: Injecting permissions for [permissionsForIdentity=${req.auth.identityId}] [type=${ActorType.IDENTITY}]`
);
} else if (req.auth.actor === ActorType.SERVICE) { } else if (req.auth.actor === ActorType.SERVICE) {
req.permission = { req.permission = {
type: ActorType.SERVICE, type: ActorType.SERVICE,
@@ -29,6 +38,10 @@ export const injectPermission = fp(async (server) => {
orgId: req.auth.orgId, orgId: req.auth.orgId,
authMethod: null authMethod: null
}; };
logger.info(
`injectPermission: Injecting permissions for [permissionsForIdentity=${req.auth.serviceTokenId}] [type=${ActorType.SERVICE}]`
);
} else if (req.auth.actor === ActorType.SCIM_CLIENT) { } else if (req.auth.actor === ActorType.SCIM_CLIENT) {
req.permission = { req.permission = {
type: ActorType.SCIM_CLIENT, type: ActorType.SCIM_CLIENT,
@@ -36,6 +49,10 @@ export const injectPermission = fp(async (server) => {
orgId: req.auth.orgId, orgId: req.auth.orgId,
authMethod: null authMethod: null
}; };
logger.info(
`injectPermission: Injecting permissions for [permissionsForIdentity=${req.auth.scimTokenId}] [type=${ActorType.SCIM_CLIENT}]`
);
} }
}); });
}); });
@@ -8,6 +8,7 @@ import { writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; 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"; import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
export const registerIntegrationRouter = async (server: FastifyZodProvider) => { 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), secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix), secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), 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), shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z secretGCPLabel: z
.object({ .object({
@@ -160,6 +165,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix), secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix), secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), 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), shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z secretGCPLabel: z
.object({ .object({
@@ -43,6 +43,11 @@ export enum IntegrationInitialSyncBehavior {
PREFER_SOURCE = "prefer-source" PREFER_SOURCE = "prefer-source"
} }
export enum IntegrationMappingBehavior {
ONE_TO_ONE = "one-to-one",
MANY_TO_ONE = "many-to-one"
}
export enum IntegrationUrls { export enum IntegrationUrls {
// integration oauth endpoints // integration oauth endpoints
GCP_TOKEN_URL = "https://oauth2.googleapis.com/token", GCP_TOKEN_URL = "https://oauth2.googleapis.com/token",
@@ -30,7 +30,12 @@ import { BadRequestError } from "@app/lib/errors";
import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types"; import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
import { TIntegrationDALFactory } from "../integration/integration-dal"; 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<string, { value: string | null; comment?: string } | null>) => const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) =>
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => { Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
@@ -570,134 +575,149 @@ const syncSecretsAWSSecretManager = async ({
accessId: string | null; accessId: string | null;
accessToken: string; accessToken: string;
}) => { }) => {
let secretsManager;
const secKeyVal = getSecretKeyValuePair(secrets);
const metadata = z.record(z.any()).parse(integration.metadata || {}); const metadata = z.record(z.any()).parse(integration.metadata || {});
try {
if (!accessId) return;
secretsManager = new SecretsManagerClient({ if (!accessId) return;
region: integration.region as string,
credentials: { const secretsManager = new SecretsManagerClient({
accessKeyId: accessId, region: integration.region as string,
secretAccessKey: accessToken credentials: {
accessKeyId: accessId,
secretAccessKey: accessToken
}
});
const processAwsSecret = async (
secretId: string,
secretValue: Record<string, string | null | undefined> | 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( if (!isEqual(secretToCompare, secretValue)) {
new GetSecretValueCommand({ await secretsManager.send(
SecretId: integration.app as string 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) { if (secretAWSTag && secretAWSTag.length) {
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString); const describedSecret = await secretsManager.send(
} // requires secretsmanager:DescribeSecret policy
new DescribeSecretCommand({
SecretId: secretId
})
);
if (!isEqual(awsSecretManagerSecretObj, secKeyVal)) { if (!describedSecret.Tags) return;
await secretsManager.send(
new UpdateSecretCommand({
SecretId: integration.app as string,
SecretString: JSON.stringify(secKeyVal)
})
);
}
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<string, string>
);
if (secretAWSTag && secretAWSTag.length) { const awsTagObj = (describedSecret.Tags || []).reduce(
const describedSecret = await secretsManager.send( (acc, item) => {
// requires secretsmanager:DescribeSecret policy if (item.Key && item.Value) {
new DescribeSecretCommand({ acc[item.Key] = item.Value;
SecretId: integration.app as string }
}) return acc;
); },
{} as Record<string, string>
);
if (!describedSecret.Tags) return; const tagsToUpdate: { Key: string; Value: string }[] = [];
const tagsToDelete: { Key: string; Value: string }[] = [];
const integrationTagObj = secretAWSTag.reduce( describedSecret.Tags?.forEach((tag) => {
(acc, item) => { if (tag.Key && tag.Value) {
acc[item.key] = item.value; if (!(tag.Key in integrationTagObj)) {
return acc; // delete tag from AWS secret manager
}, tagsToDelete.push({
{} as Record<string, string> Key: tag.Key,
); Value: tag.Value
});
const awsTagObj = (describedSecret.Tags || []).reduce( } else if (tag.Value !== integrationTagObj[tag.Key]) {
(acc, item) => { // update tag in AWS secret manager
if (item.Key && item.Value) { tagsToUpdate.push({
acc[item.Key] = item.Value; Key: tag.Key,
Value: integrationTagObj[tag.Key]
});
}
} }
return acc; });
},
{} as Record<string, string>
);
const tagsToUpdate: { Key: string; Value: string }[] = []; secretAWSTag?.forEach((tag) => {
const tagsToDelete: { Key: string; Value: string }[] = []; if (!(tag.key in awsTagObj)) {
// create tag in AWS secret manager
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({ tagsToUpdate.push({
Key: tag.Key, Key: tag.key,
Value: integrationTagObj[tag.Key] Value: tag.value
}); });
} }
} });
});
secretAWSTag?.forEach((tag) => { if (tagsToUpdate.length) {
if (!(tag.key in awsTagObj)) { await secretsManager.send(
// create tag in AWS secret manager new TagResourceCommand({
tagsToUpdate.push({ SecretId: secretId,
Key: tag.key, Tags: tagsToUpdate
Value: tag.value })
}); );
} }
});
if (tagsToUpdate.length) { if (tagsToDelete.length) {
await secretsManager.send( await secretsManager.send(
new TagResourceCommand({ new UntagResourceCommand({
SecretId: integration.app as string, SecretId: secretId,
Tags: tagsToUpdate TagKeys: tagsToDelete.map((tag) => tag.Key)
}) })
); );
}
} }
} catch (err) {
if (tagsToDelete.length) { // case when AWS manager can't find the specified secret
if (err instanceof ResourceNotFoundException && secretsManager) {
await secretsManager.send( await secretsManager.send(
new UntagResourceCommand({ new CreateSecretCommand({
SecretId: integration.app as string, Name: secretId,
TagKeys: tagsToDelete.map((tag) => tag.Key) 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) { if (metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE) {
await secretsManager.send( for await (const [key, value] of Object.entries(secrets)) {
new CreateSecretCommand({ await processAwsSecret(key, value.value);
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 }))
: []
})
);
} }
} else {
await processAwsSecret(integration.app as string, getSecretKeyValuePair(secrets));
} }
}; };
@@ -3,6 +3,7 @@ import { decryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/en
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TGetPrivateKeyDTO } from "./project-bot-types"; import { TGetPrivateKeyDTO } from "./project-bot-types";
export const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) => export const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) =>
@@ -13,11 +14,17 @@ export const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) =>
ciphertext: bot.encryptedPrivateKey ciphertext: bot.encryptedPrivateKey
}); });
export const getBotKeyFnFactory = (projectBotDAL: TProjectBotDALFactory) => { export const getBotKeyFnFactory = (
projectBotDAL: TProjectBotDALFactory,
projectDAL: Pick<TProjectDALFactory, "findById">
) => {
const getBotKeyFn = async (projectId: string) => { 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.isActive) throw new BadRequestError({ message: "Bot is not active" });
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey) if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
throw new BadRequestError({ message: "Encryption key missing" }); throw new BadRequestError({ message: "Encryption key missing" });
@@ -25,7 +25,7 @@ export const projectBotServiceFactory = ({
projectDAL, projectDAL,
permissionService permissionService
}: TProjectBotServiceFactoryDep) => { }: TProjectBotServiceFactoryDep) => {
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL); const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
const getBotKey = async (projectId: string) => { const getBotKey = async (projectId: string) => {
return getBotKeyFn(projectId); return getBotKeyFn(projectId);
@@ -1,3 +1,5 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; 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 { try {
const ghostUser = await db(TableName.ProjectMembership) const ghostUser = await (tx || db)(TableName.ProjectMembership)
.where({ projectId }) .where({ projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.select(selectAllTableCols(TableName.Users)) .select(selectAllTableCols(TableName.Users))
@@ -340,7 +340,7 @@ export const projectServiceFactory = ({
const deletedProject = await projectDAL.transaction(async (tx) => { const deletedProject = await projectDAL.transaction(async (tx) => {
const delProject = await projectDAL.deleteById(project.id, 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. // Delete the org membership for the ghost user if it's found.
if (projectGhostUser) { if (projectGhostUser) {
+2 -2
View File
@@ -608,7 +608,7 @@ export const createManySecretsRawFnFactory = ({
secretVersionTagDAL, secretVersionTagDAL,
folderDAL folderDAL
}: TCreateManySecretsRawFnFactory) => { }: TCreateManySecretsRawFnFactory) => {
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL); const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
const createManySecretsRawFn = async ({ const createManySecretsRawFn = async ({
projectId, projectId,
environment, environment,
@@ -706,7 +706,7 @@ export const updateManySecretsRawFnFactory = ({
secretVersionTagDAL, secretVersionTagDAL,
folderDAL folderDAL
}: TUpdateManySecretsRawFnFactory) => { }: TUpdateManySecretsRawFnFactory) => {
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL); const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
const updateManySecretsRawFn = async ({ const updateManySecretsRawFn = async ({
projectId, projectId,
environment, environment,
@@ -7,7 +7,6 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
@@ -167,15 +166,11 @@ export const serviceTokenServiceFactory = ({
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceToken.secretHash); const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceToken.secretHash);
if (!isMatch) throw new UnauthorizedError(); if (!isMatch) throw new UnauthorizedError();
// const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, { const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, {
// lastUsed: new Date() lastUsed: new Date()
// }); });
logger.info( return { ...serviceToken, lastUsed: updatedToken.lastUsed, orgId: project.orgId };
`fnValidateServiceToken: [serviceToken=${serviceToken.id}] [serviceTokenProjectId=${serviceToken.projectId}]`
);
return { ...serviceToken, lastUsed: serviceToken.lastUsed, orgId: project.orgId };
}; };
return { return {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 142 KiB

@@ -72,6 +72,9 @@ Prerequisites:
<ParamField path="AWS Region" type="string" required> <ParamField path="AWS Region" type="string" required>
The region that you want to integrate with in AWS Secrets Manager. The region that you want to integrate with in AWS Secrets Manager.
</ParamField> </ParamField>
<ParamField path="Mapping Behavior" type="string" required>
How you want the integration to map the secrets. The selected value could be either one to one or one to many.
</ParamField>
<ParamField path="AWS SM Secret Name" type="string" required> <ParamField path="AWS SM Secret Name" type="string" required>
The secret name/path in AWS into which you want to sync the secrets from Infisical. The secret name/path in AWS into which you want to sync the secrets from Infisical.
</ParamField> </ParamField>
@@ -64,6 +64,7 @@ export const useCreateIntegration = () => {
secretSuffix?: string; secretSuffix?: string;
initialSyncBehavior?: string; initialSyncBehavior?: string;
shouldAutoRedeploy?: boolean; shouldAutoRedeploy?: boolean;
mappingBehavior?: string;
secretAWSTag?: { secretAWSTag?: {
key: string; key: string;
value: string; value: string;
@@ -36,6 +36,7 @@ export type TIntegration = {
metadata?: { metadata?: {
secretSuffix?: string; secretSuffix?: string;
syncBehavior?: IntegrationSyncBehavior; syncBehavior?: IntegrationSyncBehavior;
mappingBehavior?: IntegrationMappingBehavior;
scope: string; scope: string;
org: string; org: string;
project: string; project: string;
@@ -48,3 +49,8 @@ export enum IntegrationSyncBehavior {
PREFER_TARGET = "prefer-target", PREFER_TARGET = "prefer-target",
PREFER_SOURCE = "prefer-source" PREFER_SOURCE = "prefer-source"
} }
export enum IntegrationMappingBehavior {
ONE_TO_ONE = "one-to-one",
MANY_TO_ONE = "many-to-one"
}
@@ -15,6 +15,7 @@ import queryString from "query-string";
import { useCreateIntegration } from "@app/hooks/api"; import { useCreateIntegration } from "@app/hooks/api";
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries"; import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
import { import {
Button, Button,
@@ -70,6 +71,17 @@ const awsRegions = [
{ name: "AWS GovCloud (US-West)", slug: "us-gov-west-1" } { 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() { export default function AWSSecretManagerCreateIntegrationPage() {
const router = useRouter(); const router = useRouter();
const { mutateAsync } = useCreateIntegration(); const { mutateAsync } = useCreateIntegration();
@@ -84,6 +96,9 @@ export default function AWSSecretManagerCreateIntegrationPage() {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [secretPath, setSecretPath] = useState("/"); const [secretPath, setSecretPath] = useState("/");
const [selectedAWSRegion, setSelectedAWSRegion] = useState(""); const [selectedAWSRegion, setSelectedAWSRegion] = useState("");
const [selectedMappingBehavior, setSelectedMappingBehavior] = useState(
IntegrationMappingBehavior.MANY_TO_ONE
);
const [targetSecretName, setTargetSecretName] = useState(""); const [targetSecretName, setTargetSecretName] = useState("");
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState(""); const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState("");
const [tagKey, setTagKey] = useState(""); const [tagKey, setTagKey] = useState("");
@@ -116,7 +131,14 @@ export default function AWSSecretManagerCreateIntegrationPage() {
const handleButtonClick = async () => { const handleButtonClick = async () => {
try { try {
if (targetSecretName.trim() === "") { if (!selectedMappingBehavior) {
return;
}
if (
selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE &&
targetSecretName.trim() === ""
) {
setTargetSecretName("Secret name cannot be blank"); setTargetSecretName("Secret name cannot be blank");
return; return;
} }
@@ -143,7 +165,8 @@ export default function AWSSecretManagerCreateIntegrationPage() {
] ]
} }
: {}), : {}),
...(kmsKeyId && { kmsKeyId }) ...(kmsKeyId && { kmsKeyId }),
mappingBehavior: selectedMappingBehavior
} }
}); });
@@ -248,19 +271,40 @@ export default function AWSSecretManagerCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl <FormControl label="Mapping Behavior">
label="AWS SM Secret Name" <Select
errorText={targetSecretNameErrorText} value={selectedMappingBehavior}
isError={targetSecretNameErrorText !== "" ?? false} onValueChange={(val) => {
> setSelectedMappingBehavior(val as IntegrationMappingBehavior);
<Input }}
placeholder={`${workspace.name className="w-full border border-mineshaft-500 text-left"
.toLowerCase() >
.replace(/ /g, "-")}/${selectedSourceEnvironment}`} {mappingBehaviors.map((option) => (
value={targetSecretName} <SelectItem
onChange={(e) => setTargetSecretName(e.target.value)} value={option.value}
/> className="text-left"
key={`aws-environment-${option.value}`}
>
{option.label}
</SelectItem>
))}
</Select>
</FormControl> </FormControl>
{selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE && (
<FormControl
label="AWS SM Secret Name"
errorText={targetSecretNameErrorText}
isError={targetSecretNameErrorText !== "" ?? false}
>
<Input
placeholder={`${workspace.name
.toLowerCase()
.replace(/ /g, "-")}/${selectedSourceEnvironment}`}
value={targetSecretName}
onChange={(e) => setTargetSecretName(e.target.value)}
/>
</FormControl>
)}
</motion.div> </motion.div>
</TabPanel> </TabPanel>
<TabPanel value={TabSections.Options}> <TabPanel value={TabSections.Options}>
@@ -121,6 +121,14 @@ export default function LoginPage() {
} }
}, [router]); }, [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) { if (userLoading || !user) {
return <LoadingScreen />; return <LoadingScreen />;
} }
@@ -21,6 +21,7 @@ import {
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useSyncIntegration } from "@app/hooks/api/integrations/queries"; import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
import { TIntegration } from "@app/hooks/api/types"; import { TIntegration } from "@app/hooks/api/types";
type Props = { type Props = {
@@ -131,30 +132,35 @@ export const IntegrationsSection = ({
</div> </div>
</div> </div>
)} )}
<div className="ml-2 flex flex-col"> {!(
<FormLabel integration.integration === "aws-secret-manager" &&
label={ integration.metadata?.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE
(integration.integration === "qovery" && integration?.scope) || ) && (
(integration.integration === "aws-secret-manager" && "Secret") || <div className="ml-2 flex flex-col">
(integration.integration === "aws-parameter-store" && "Path") || <FormLabel
(integration?.integration === "terraform-cloud" && "Project") || label={
(integration?.scope === "github-org" && "Organization") || (integration.integration === "qovery" && integration?.scope) ||
(["github-repo", "github-env"].includes(integration?.scope as string) && (integration.integration === "aws-secret-manager" && "Secret") ||
"Repository") || (integration.integration === "aws-parameter-store" && "Path") ||
"App" (integration?.integration === "terraform-cloud" && "Project") ||
} (integration?.scope === "github-org" && "Organization") ||
/> (["github-repo", "github-env"].includes(integration?.scope as string) &&
<div className="no-scrollbar::-webkit-scrollbar min-w-[8rem] max-w-[12rem] overflow-scroll whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200 no-scrollbar"> "Repository") ||
{(integration.integration === "hashicorp-vault" && "App"
`${integration.app} - path: ${integration.path}`) || }
(integration.scope === "github-org" && `${integration.owner}`) || />
(integration.integration === "aws-parameter-store" && <div className="no-scrollbar::-webkit-scrollbar min-w-[8rem] max-w-[12rem] overflow-scroll whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200 no-scrollbar">
`${integration.path}`) || {(integration.integration === "hashicorp-vault" &&
(integration.scope?.startsWith("github-") && `${integration.app} - path: ${integration.path}`) ||
`${integration.owner}/${integration.app}`) || (integration.scope === "github-org" && `${integration.owner}`) ||
integration.app} (integration.integration === "aws-parameter-store" &&
`${integration.path}`) ||
(integration.scope?.startsWith("github-") &&
`${integration.owner}/${integration.app}`) ||
integration.app}
</div>
</div> </div>
</div> )}
{(integration.integration === "vercel" || {(integration.integration === "vercel" ||
integration.integration === "netlify" || integration.integration === "netlify" ||
integration.integration === "railway" || integration.integration === "railway" ||