Merge remote-tracking branch 'origin' into groups-phase-2b

This commit is contained in:
Tuan Dang
2024-04-22 10:37:53 -07:00
53 changed files with 1112 additions and 450 deletions
+1 -3
View File
@@ -19,7 +19,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
description: "Return project secret snapshots ids",
security: [
{
apiKeyAuth: [],
bearerAuth: []
}
],
@@ -97,8 +96,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
description: "Return audit logs",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -69,7 +69,6 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
description: "Roll back project secrets to those captured in a secret snapshot version.",
security: [
{
apiKeyAuth: [],
bearerAuth: []
}
],
@@ -2,7 +2,8 @@ import { z } from "zod";
export enum SqlProviders {
Postgres = "postgres",
MySQL = "mysql2"
MySQL = "mysql2",
Oracle = "oracledb"
}
export const DynamicSecretSqlDBSchema = z.object({
@@ -8,15 +8,25 @@ import { BadRequestError } from "@app/lib/errors";
import { getDbConnectionHost } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretSqlDBSchema, TDynamicProviderFns } from "./models";
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
const generatePassword = (size?: number) => {
const generatePassword = (provider: SqlProviders) => {
// oracle has limit of 48 password length
const size = provider === SqlProviders.Oracle ? 30 : 48;
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 48)(size);
};
const generateUsername = (provider: SqlProviders) => {
// For oracle, the client assumes everything is upper case when not using quotes around the password
if (provider === SqlProviders.Oracle) return alphaNumericNanoId(32).toUpperCase();
return alphaNumericNanoId(32);
};
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
@@ -59,10 +69,10 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const db = await getClient(providerInputs);
const isConnected = await db
.raw("SELECT NOW()")
.then(() => true)
.catch(() => false);
// oracle needs from keyword
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
const isConnected = await db.raw(testStatement).then(() => true);
await db.destroy();
return isConnected;
};
@@ -71,8 +81,8 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const providerInputs = await validateProviderInputs(inputs);
const db = await getClient(providerInputs);
const username = alphaNumericNanoId(32);
const password = generatePassword();
const username = generateUsername(providerInputs.client);
const password = generatePassword(providerInputs.client);
const { database } = providerInputs;
const expiration = new Date(expireAt).toISOString();
@@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, subject } from "@casl/ability";
import { TableName, TSecretTagJunctionInsert } from "@app/db/schemas";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
@@ -23,6 +23,7 @@ import {
import { TSnapshotDALFactory } from "./snapshot-dal";
import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal";
import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal";
import { getFullFolderPath } from "./snapshot-service-fns";
type TSecretSnapshotServiceFactoryDep = {
snapshotDAL: TSnapshotDALFactory;
@@ -33,7 +34,7 @@ type TSecretSnapshotServiceFactoryDep = {
secretDAL: Pick<TSecretDALFactory, "delete" | "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
folderDAL: Pick<TSecretFolderDALFactory, "findById" | "findBySecretPath" | "delete" | "insertMany">;
folderDAL: Pick<TSecretFolderDALFactory, "findById" | "findBySecretPath" | "delete" | "insertMany" | "find">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "isValidLicense">;
};
@@ -71,6 +72,12 @@ export const secretSnapshotServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
@@ -98,6 +105,12 @@ export const secretSnapshotServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
@@ -116,6 +129,19 @@ export const secretSnapshotServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: snapshot.folderId,
envId: snapshot.environment.id
});
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: snapshot.environment.slug, secretPath: fullFolderPath })
);
return snapshot;
};
@@ -101,6 +101,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
key: "snapshotId",
parentMapper: ({
snapshotId: id,
folderId,
projectId,
envId,
envSlug,
@@ -109,6 +110,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
snapshotUpdatedAt: updatedAt
}) => ({
id,
folderId,
projectId,
createdAt,
updatedAt,
@@ -0,0 +1,28 @@
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
type GetFullFolderPath = {
folderDAL: Pick<TSecretFolderDALFactory, "findById" | "find">; // Added findAllInEnv
folderId: string;
envId: string;
};
export const getFullFolderPath = async ({ folderDAL, folderId, envId }: GetFullFolderPath): Promise<string> => {
// Helper function to remove duplicate slashes
const removeDuplicateSlashes = (path: string) => path.replace(/\/{2,}/g, "/");
// Fetch all folders at once based on environment ID to avoid multiple queries
const folders = await folderDAL.find({ envId });
const folderMap = new Map(folders.map((folder) => [folder.id, folder]));
const buildPath = (currFolderId: string): string => {
const folder = folderMap.get(currFolderId);
if (!folder) return "";
const folderPathSegment = !folder.parentId && folder.name === "root" ? "/" : `/${folder.name}`;
if (folder.parentId) {
return removeDuplicateSlashes(`${buildPath(folder.parentId)}${folderPathSegment}`);
}
return removeDuplicateSlashes(folderPathSegment);
};
return buildPath(folderId);
};
+2 -1
View File
@@ -589,7 +589,8 @@ export const INTEGRATION = {
secretSuffix: "The suffix for the saved secret. Used by GCP",
initialSyncBehavoir: "Type of syncing behavoir with the integration",
shouldAutoRedeploy: "Used by Render to trigger auto deploy",
secretGCPLabel: "The label for the GCP secrets"
secretGCPLabel: "The label for the GCP secrets",
secretAWSTag: "The tag for the AWS secrets"
}
},
UPDATE: {
-6
View File
@@ -30,12 +30,6 @@ export const fastifySwagger = fp(async (fastify) => {
scheme: "bearer",
bearerFormat: "JWT",
description: "An access token in Infisical"
},
apiKeyAuth: {
type: "apiKey",
in: "header",
name: "X-API-Key",
description: "An API Key in Infisical"
}
}
}
@@ -56,7 +56,14 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
labelValue: z.string()
})
.optional()
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel)
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
secretAWSTag: z
.object({
key: z.string(),
value: z.string()
})
.optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag)
})
.optional()
}),
@@ -18,8 +18,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
description: "Create environment",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -77,8 +76,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
description: "Update environment",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -144,8 +142,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
description: "Delete environment",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -26,8 +26,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
description: "Return project user memberships",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -139,8 +138,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
description: "Update project user membership",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -213,8 +211,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
description: "Delete project user membership",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -138,6 +138,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit
},
schema: {
description: "Get project",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.GET.workspaceId)
}),
@@ -170,6 +176,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: writeLimit
},
schema: {
description: "Delete project",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.DELETE.workspaceId)
}),
@@ -239,6 +251,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: writeLimit
},
schema: {
description: "Update project",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.UPDATE.workspaceId)
}),
@@ -19,8 +19,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
description: "Create folders",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
body: z.object({
@@ -76,8 +75,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
description: "Update folder",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -140,8 +138,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
description: "Delete a folder",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -200,8 +197,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
description: "Get folders",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
querystring: z.object({
@@ -19,8 +19,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
description: "Create secret imports",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
body: z.object({
@@ -84,8 +83,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
description: "Update secret imports",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -159,8 +157,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
description: "Delete secret imports",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -223,8 +220,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
description: "Get secret imports",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
querystring: z.object({
@@ -18,8 +18,7 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
description: "Return organization identity memberships",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -17,8 +17,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
description: "Return organization user memberships",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -65,8 +64,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
description: "Return projects in organization that user is part of",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -114,8 +112,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
description: "Update organization user memberships",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -157,8 +154,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
description: "Delete organization user memberships",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -15,6 +15,12 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
rateLimit: writeLimit
},
schema: {
description: "Invite members to project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().describe(PROJECTS.INVITE_MEMBER.projectId)
}),
@@ -64,10 +70,15 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
rateLimit: writeLimit
},
schema: {
description: "Remove members from project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().describe(PROJECTS.REMOVE_MEMBER.projectId)
}),
body: z.object({
emails: z.string().email().array().default([]).describe(PROJECTS.REMOVE_MEMBER.emails),
usernames: z.string().array().default([]).describe(PROJECTS.REMOVE_MEMBER.usernames)
+12 -5
View File
@@ -36,11 +36,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
schema: {
description: "Return encrypted project key",
security: [
{
apiKeyAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.GET_KEY.workspaceId)
}),
@@ -149,6 +144,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: creationLimit
},
schema: {
description: "Create a new project",
security: [
{
bearerAuth: []
}
],
body: z.object({
projectName: z.string().trim().describe(PROJECTS.CREATE.projectName),
slug: z
@@ -200,6 +201,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: writeLimit
},
schema: {
description: "Delete project",
security: [
{
bearerAuth: []
}
],
params: z.object({
slug: slugSchema.describe("The slug of the project to delete.")
}),
@@ -85,11 +85,6 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
},
schema: {
description: "Return organizations that current user is part of",
security: [
{
apiKeyAuth: []
}
],
response: {
200: z.object({
organizations: OrganizationsSchema.array()
@@ -217,11 +212,6 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
},
schema: {
description: "Retrieve the current user on the request",
security: [
{
apiKeyAuth: []
}
],
response: {
200: z.object({
user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true }))
+5 -10
View File
@@ -158,8 +158,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
description: "List secrets",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
querystring: z.object({
@@ -280,8 +279,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
description: "Get a secret by name",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -375,8 +373,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
description: "Create secret",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -464,8 +461,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
description: "Update secret",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -550,8 +546,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
description: "Delete secret",
security: [
{
bearerAuth: [],
apiKeyAuth: []
bearerAuth: []
}
],
params: z.object({
@@ -129,26 +129,55 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
* Return list of names of apps for Vercel integration
*/
const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => {
const res = (
await request.get<{ projects: { name: string; id: string }[] }>(`${IntegrationUrls.VERCEL_API_URL}/v9/projects`, {
const apps: Array<{ name: string; appId: string }> = [];
const limit = "20";
let hasMorePages = true;
let next: number | null = null;
interface Response {
projects: { name: string; id: string }[];
pagination: {
count: number;
next: number | null;
prev: number;
};
}
while (hasMorePages) {
const params: { [key: string]: string } = {
limit
};
if (teamId) {
params.teamId = teamId;
}
if (next) {
params.until = String(next);
}
const { data } = await request.get<Response>(`${IntegrationUrls.VERCEL_API_URL}/v9/projects`, {
params: new URLSearchParams(params),
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
},
...(teamId
? {
params: {
teamId
}
}
: {})
})
).data;
}
});
const apps = res.projects.map((a) => ({
name: a.name,
appId: a.id
}));
data.projects.forEach((a) => {
apps.push({
name: a.name,
appId: a.id
});
});
next = data.pagination.next;
if (data.pagination.next === null) {
hasMorePages = false;
}
}
return apps;
};
@@ -457,6 +457,8 @@ const syncSecretsAWSParameterStore = async ({
});
ssm.config.update(config);
const metadata = z.record(z.any()).parse(integration.metadata);
const params = {
Path: integration.path as string,
Recursive: false,
@@ -486,7 +488,8 @@ const syncSecretsAWSParameterStore = async ({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key].value,
Overwrite: true
// Overwrite: true,
Tags: metadata.secretAWSTag ? [{ Key: metadata.secretAWSTag.key, Value: metadata.secretAWSTag.value }] : []
})
.promise();
// case: secret exists in AWS parameter store
@@ -499,6 +502,7 @@ const syncSecretsAWSParameterStore = async ({
Type: "SecureString",
Value: secrets[key].value,
Overwrite: true
// Tags: metadata.secretAWSTag ? [{ Key: metadata.secretAWSTag.key, Value: metadata.secretAWSTag.value }] : []
})
.promise();
}
@@ -537,6 +541,7 @@ const syncSecretsAWSSecretManager = async ({
}) => {
let secretsManager;
const secKeyVal = getSecretKeyValuePair(secrets);
const metadata = z.record(z.any()).parse(integration.metadata);
try {
if (!accessId) return;
@@ -573,7 +578,8 @@ const syncSecretsAWSSecretManager = async ({
await secretsManager.send(
new CreateSecretCommand({
Name: integration.app as string,
SecretString: JSON.stringify(secKeyVal)
SecretString: JSON.stringify(secKeyVal),
Tags: metadata.secretAWSTag ? [{ Key: metadata.secretAWSTag.key, Value: metadata.secretAWSTag.value }] : []
})
);
}
@@ -22,6 +22,10 @@ export type TCreateIntegrationDTO = {
labelName: string;
labelValue: string;
};
secretAWSTag?: {
key: string;
value: string;
};
};
} & Omit<TProjectPermission, "projectId">;
@@ -134,8 +134,7 @@ export const projectMembershipServiceFactory = ({
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ userId }) => ({
projectId,
userId: userId as string,
role: ProjectMembershipRole.Member
userId: userId as string
})),
tx
);
@@ -267,8 +266,7 @@ export const projectMembershipServiceFactory = ({
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ user }) => ({
projectId,
userId: user.id,
role: ProjectMembershipRole.Member
userId: user.id
})),
tx
);