diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 1ece89639..737067534 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -1,6 +1,7 @@ ARG POSTHOG_HOST=https://app.posthog.com ARG POSTHOG_API_KEY=posthog-api-key ARG INTERCOM_ID=intercom-id +ARG SAML_ORG_SLUG=saml-org-slug-default FROM node:20-alpine AS base @@ -35,6 +36,8 @@ ARG INTERCOM_ID ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID ARG INFISICAL_PLATFORM_VERSION ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION +ARG SAML_ORG_SLUG +ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG # Build RUN npm run build @@ -100,6 +103,9 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ ARG INTERCOM_ID=intercom-id ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID +ARG SAML_ORG_SLUG +ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \ + BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG WORKDIR / diff --git a/backend/scripts/create-backend-file.ts b/backend/scripts/create-backend-file.ts index b821aba21..fb71994ce 100644 --- a/backend/scripts/create-backend-file.ts +++ b/backend/scripts/create-backend-file.ts @@ -103,11 +103,15 @@ export const ${dalName} = (db: TDbClient) => { `import { z } from "zod"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { readLimit } from "@app/server/config/rateLimiter"; export const register${pascalCase}Router = async (server: FastifyZodProvider) => { server.route({ - url: "/", method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, schema: { params: z.object({}), response: { diff --git a/backend/scripts/create-migration.ts b/backend/scripts/create-migration.ts index f4017b12b..59040a37a 100644 --- a/backend/scripts/create-migration.ts +++ b/backend/scripts/create-migration.ts @@ -7,10 +7,10 @@ const prompt = promptSync({ sigint: true }); const migrationName = prompt("Enter name for migration: "); +// Remove spaces from migration name and replace with hyphens +const formattedMigrationName = migrationName.replace(/\s+/g, "-"); + execSync( - `npx knex migrate:make --knexfile ${path.join( - __dirname, - "../src/db/knexfile.ts" - )} -x ts ${migrationName}`, + `npx knex migrate:make --knexfile ${path.join(__dirname, "../src/db/knexfile.ts")} -x ts ${formattedMigrationName}`, { stdio: "inherit" } ); diff --git a/backend/src/db/migrations/20240405000045_org-memberships-unique-constraint.ts b/backend/src/db/migrations/20240405000045_org-memberships-unique-constraint.ts new file mode 100644 index 000000000..9a342d542 --- /dev/null +++ b/backend/src/db/migrations/20240405000045_org-memberships-unique-constraint.ts @@ -0,0 +1,111 @@ +import { Knex } from "knex"; +import { z } from "zod"; + +import { TableName, TOrgMemberships } from "../schemas"; + +const validateOrgMembership = (membershipToValidate: TOrgMemberships, firstMembership: TOrgMemberships) => { + const firstOrgId = firstMembership.orgId; + const firstUserId = firstMembership.userId; + + if (membershipToValidate.id === firstMembership.id) { + return; + } + + if (membershipToValidate.inviteEmail !== firstMembership.inviteEmail) { + throw new Error(`Invite emails are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`); + } + if (membershipToValidate.orgId !== firstMembership.orgId) { + throw new Error(`OrgIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`); + } + if (membershipToValidate.role !== firstMembership.role) { + throw new Error(`Roles are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`); + } + if (membershipToValidate.roleId !== firstMembership.roleId) { + throw new Error(`RoleIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`); + } + if (membershipToValidate.status !== firstMembership.status) { + throw new Error(`Statuses are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`); + } + if (membershipToValidate.userId !== firstMembership.userId) { + throw new Error(`UserIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`); + } +}; + +export async function up(knex: Knex): Promise { + const RowSchema = z.object({ + userId: z.string(), + orgId: z.string(), + cnt: z.string() + }); + + // Transactional find and delete duplicate rows + await knex.transaction(async (tx) => { + const duplicateRows = await tx(TableName.OrgMembership) + .select("userId", "orgId") // Select the userId and orgId so we can group by them + .count("* as cnt") // Count the number of rows for each userId and orgId, so we can make sure there are more than 1 row (a duplicate) + .groupBy("userId", "orgId") + .havingRaw("count(*) > ?", [1]); // Using havingRaw for direct SQL expressions + + // Parse the rows to ensure they are in the correct format, and for type safety + const parsedRows = RowSchema.array().parse(duplicateRows); + + // For each of the duplicate rows, loop through and find the actual memberships to delete + for (const row of parsedRows) { + const count = Number(row.cnt); + + // An extra check to ensure that the count is actually a number, and the number is greater than 2 + if (typeof count !== "number" || count < 2) { + // eslint-disable-next-line no-continue + continue; + } + + // Find all the organization memberships that have the same userId and orgId + // eslint-disable-next-line no-await-in-loop + const rowsToDelete = await tx(TableName.OrgMembership).where({ + userId: row.userId, + orgId: row.orgId + }); + + // Ensure that all the rows have exactly the same value, except id, createdAt, updatedAt + for (const rowToDelete of rowsToDelete) { + validateOrgMembership(rowToDelete, rowsToDelete[0]); + } + + // Find the row with the latest createdAt, which we will keep + + let lowestCreatedAt: number | null = null; + let latestCreatedRow: TOrgMemberships | null = null; + + for (const rowToDelete of rowsToDelete) { + if (lowestCreatedAt === null || rowToDelete.createdAt.getTime() < lowestCreatedAt) { + lowestCreatedAt = rowToDelete.createdAt.getTime(); + latestCreatedRow = rowToDelete; + } + } + if (!latestCreatedRow) { + throw new Error("Failed to find last created membership"); + } + + // Filter out the latest row from the rows to delete + const membershipIdsToDelete = rowsToDelete.map((r) => r.id).filter((id) => id !== latestCreatedRow!.id); + + // eslint-disable-next-line no-await-in-loop + const numberOfRowsDeleted = await tx(TableName.OrgMembership).whereIn("id", membershipIdsToDelete).delete(); + + // eslint-disable-next-line no-console + console.log( + `Deleted ${numberOfRowsDeleted} duplicate organization memberships for ${row.userId} and ${row.orgId}` + ); + } + }); + + await knex.schema.alterTable(TableName.OrgMembership, (table) => { + table.unique(["userId", "orgId"]); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(TableName.OrgMembership, (table) => { + table.dropUnique(["userId", "orgId"]); + }); +} diff --git a/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts b/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts index c830e2a7f..5ef9f7eeb 100644 --- a/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts +++ b/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts @@ -5,14 +5,18 @@ import { DynamicSecretLeasesSchema } from "@app/db/schemas"; import { DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs"; import { daysToMillisecond } from "@app/lib/dates"; import { removeTrailingSlash } from "@app/lib/fn"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/", method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ dynamicSecretName: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.dynamicSecretName).toLowerCase(), @@ -55,8 +59,11 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide }); server.route({ - url: "/:leaseId", method: "DELETE", + url: "/:leaseId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.DELETE.leaseId) @@ -94,8 +101,11 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide }); server.route({ - url: "/:leaseId/renew", method: "POST", + url: "/:leaseId/renew", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.leaseId) @@ -146,6 +156,9 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide server.route({ url: "/:leaseId", method: "GET", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.leaseId) diff --git a/backend/src/ee/routes/v1/dynamic-secret-router.ts b/backend/src/ee/routes/v1/dynamic-secret-router.ts index 0cbf8a312..049370743 100644 --- a/backend/src/ee/routes/v1/dynamic-secret-router.ts +++ b/backend/src/ee/routes/v1/dynamic-secret-router.ts @@ -7,14 +7,18 @@ import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/pro import { DYNAMIC_SECRETS } from "@app/lib/api-docs"; import { daysToMillisecond } from "@app/lib/dates"; import { removeTrailingSlash } from "@app/lib/fn"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/", method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.CREATE.projectSlug), @@ -74,8 +78,11 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => }); server.route({ - url: "/:name", method: "PATCH", + url: "/:name", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.UPDATE.name) @@ -138,8 +145,11 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => }); server.route({ - url: "/:name", method: "DELETE", + url: "/:name", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.DELETE.name) @@ -173,6 +183,9 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => server.route({ url: "/:name", method: "GET", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ name: z.string().min(1).describe(DYNAMIC_SECRETS.GET_BY_NAME.name) @@ -207,6 +220,9 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => server.route({ url: "/", method: "GET", + config: { + rateLimit: readLimit + }, schema: { querystring: z.object({ projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST.projectSlug), @@ -235,6 +251,9 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => server.route({ url: "/:name/leases", method: "GET", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ name: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.name) diff --git a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts index 4863a506e..a1a2e36fa 100644 --- a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts @@ -9,14 +9,24 @@ import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/service import { ProjectPermissionSet } from "@app/ee/services/permission/project-permission"; import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/permanent", method: "POST", + url: "/permanent", + config: { + rateLimit: writeLimit + }, schema: { + description: "Create a permanent or a non expiry specific privilege for identity.", + security: [ + { + bearerAuth: [] + } + ], body: z.object({ identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId), projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug), @@ -25,11 +35,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F .min(1) .max(60) .trim() - .default(slugify(alphaNumericNanoId(12))) .refine((val) => val.toLowerCase() === val, "Must be lowercase") .refine((v) => slugify(v) === v, { message: "Slug must be a valid slug" }) + .optional() .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions) }), @@ -47,6 +57,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, ...req.body, + slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), isTemporary: false, permissions: JSON.stringify(packRules(req.body.permissions)) }); @@ -55,9 +66,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }); server.route({ - url: "/temporary", method: "POST", + url: "/temporary", + config: { + rateLimit: writeLimit + }, schema: { + description: "Create a temporary or a expiring specific privilege for identity.", + security: [ + { + bearerAuth: [] + } + ], body: z.object({ identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId), projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug), @@ -66,11 +86,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F .min(1) .max(60) .trim() - .default(slugify(alphaNumericNanoId(12))) .refine((val) => val.toLowerCase() === val, "Must be lowercase") .refine((v) => slugify(v) === v, { message: "Slug must be a valid slug" }) + .optional() .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions), temporaryMode: z @@ -99,6 +119,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, ...req.body, + slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), isTemporary: true, permissions: JSON.stringify(packRules(req.body.permissions)) }); @@ -107,9 +128,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }); server.route({ - url: "/", method: "PATCH", + url: "/", + config: { + rateLimit: writeLimit + }, schema: { + description: "Update a specific privilege of an identity.", + security: [ + { + bearerAuth: [] + } + ], body: z.object({ // disallow empty string privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.slug), @@ -170,9 +200,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }); server.route({ - url: "/", method: "DELETE", + url: "/", + config: { + rateLimit: writeLimit + }, schema: { + description: "Delete a specific privilege of an identity.", + security: [ + { + bearerAuth: [] + } + ], body: z.object({ privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.slug), identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.identityId), @@ -200,9 +239,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }); server.route({ - url: "/:privilegeSlug", method: "GET", + url: "/:privilegeSlug", + config: { + rateLimit: readLimit + }, schema: { + description: "Retrieve details of a specific privilege by privilege slug.", + security: [ + { + bearerAuth: [] + } + ], params: z.object({ privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.slug) }), @@ -231,9 +279,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }); server.route({ - url: "/", method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, schema: { + description: "List of a specific privilege of an identity in a project.", + security: [ + { + bearerAuth: [] + } + ], querystring: z.object({ identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.identityId), projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.projectSlug), diff --git a/backend/src/ee/routes/v1/ldap-router.ts b/backend/src/ee/routes/v1/ldap-router.ts index d160f97c6..c35d275ae 100644 --- a/backend/src/ee/routes/v1/ldap-router.ts +++ b/backend/src/ee/routes/v1/ldap-router.ts @@ -17,6 +17,7 @@ import { z } from "zod"; import { LdapConfigsSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -97,8 +98,11 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/config", method: "GET", + url: "/config", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { querystring: z.object({ @@ -130,8 +134,11 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/config", method: "POST", + url: "/config", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { body: z.object({ @@ -164,6 +171,9 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { server.route({ url: "/config", method: "PATCH", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { body: z diff --git a/backend/src/ee/routes/v1/license-router.ts b/backend/src/ee/routes/v1/license-router.ts index bd3fdca18..fbf1af43b 100644 --- a/backend/src/ee/routes/v1/license-router.ts +++ b/backend/src/ee/routes/v1/license-router.ts @@ -3,13 +3,17 @@ // TODO(akhilmhdh): Fix this when licence service gets it type import { z } from "zod"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerLicenseRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/:organizationId/plans/table", method: "GET", + url: "/:organizationId/plans/table", + config: { + rateLimit: readLimit + }, schema: { querystring: z.object({ billingCycle: z.enum(["monthly", "yearly"]) }), params: z.object({ organizationId: z.string().trim() }), @@ -32,8 +36,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/plan", method: "GET", + url: "/:organizationId/plan", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), response: { @@ -54,8 +61,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/plans", method: "GET", + url: "/:organizationId/plans", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), querystring: z.object({ workspaceId: z.string().trim().optional() }), @@ -77,8 +87,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/session/trial", method: "POST", + url: "/:organizationId/session/trial", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), body: z.object({ success_url: z.string().trim() }), @@ -103,6 +116,9 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { server.route({ url: "/:organizationId/customer-portal-session", method: "POST", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), response: { @@ -123,8 +139,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/plan/billing", method: "GET", + url: "/:organizationId/plan/billing", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), response: { @@ -145,8 +164,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/plan/table", method: "GET", + url: "/:organizationId/plan/table", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), response: { @@ -167,8 +189,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/billing-details", method: "GET", + url: "/:organizationId/billing-details", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), response: { @@ -189,8 +214,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/billing-details", method: "PATCH", + url: "/:organizationId/billing-details", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), body: z.object({ @@ -217,8 +245,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/billing-details/payment-methods", method: "GET", + url: "/:organizationId/billing-details/payment-methods", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), response: { @@ -239,8 +270,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/billing-details/payment-methods", method: "POST", + url: "/:organizationId/billing-details/payment-methods", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), body: z.object({ @@ -267,8 +301,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/billing-details/payment-methods/:pmtMethodId", method: "DELETE", + url: "/:organizationId/billing-details/payment-methods/:pmtMethodId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim(), @@ -293,8 +330,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/billing-details/tax-ids", method: "GET", + url: "/:organizationId/billing-details/tax-ids", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() @@ -317,8 +357,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/billing-details/tax-ids", method: "POST", + url: "/:organizationId/billing-details/tax-ids", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim() @@ -347,8 +390,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/billing-details/tax-ids/:taxId", method: "DELETE", + url: "/:organizationId/billing-details/tax-ids/:taxId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim(), @@ -373,8 +419,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/invoices", method: "GET", + url: "/:organizationId/invoices", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() @@ -397,8 +446,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:organizationId/licenses", method: "GET", + url: "/:organizationId/licenses", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() diff --git a/backend/src/ee/routes/v1/org-role-router.ts b/backend/src/ee/routes/v1/org-role-router.ts index 3ecf6fbfd..380f61e23 100644 --- a/backend/src/ee/routes/v1/org-role-router.ts +++ b/backend/src/ee/routes/v1/org-role-router.ts @@ -2,6 +2,7 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -9,6 +10,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/:organizationId/roles", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim() @@ -51,6 +55,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", url: "/:organizationId/roles/:roleId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim(), @@ -95,6 +102,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", url: "/:organizationId/roles/:roleId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim(), @@ -122,6 +132,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:organizationId/roles", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() @@ -151,6 +164,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:organizationId/permissions", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts index a08a9851b..c1470e7ba 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -8,6 +9,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/:projectId/roles", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ projectId: z.string().trim() @@ -41,6 +45,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", url: "/:projectId/roles/:roleId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ projectId: z.string().trim(), @@ -76,6 +83,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", url: "/:projectId/roles/:roleId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ projectId: z.string().trim(), @@ -104,6 +114,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:projectId/roles", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ projectId: z.string().trim() @@ -134,6 +147,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:projectId/permissions", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ projectId: z.string().trim() diff --git a/backend/src/ee/routes/v1/project-router.ts b/backend/src/ee/routes/v1/project-router.ts index 3fbe3f026..bb81fd8ee 100644 --- a/backend/src/ee/routes/v1/project-router.ts +++ b/backend/src/ee/routes/v1/project-router.ts @@ -4,6 +4,7 @@ import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; +import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -11,6 +12,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:workspaceId/secret-snapshots", + config: { + rateLimit: readLimit + }, schema: { description: "Return project secret snapshots ids", security: [ @@ -51,6 +55,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:workspaceId/secret-snapshots/count", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ workspaceId: z.string().trim() @@ -83,6 +90,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:workspaceId/audit-logs", + config: { + rateLimit: readLimit + }, schema: { description: "Return audit logs", security: [ @@ -145,6 +155,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:workspaceId/audit-logs/filters/actors", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ workspaceId: z.string().trim() diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index b30afda13..6cae30f7a 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -17,6 +17,7 @@ import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -203,8 +204,11 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/config", method: "GET", + url: "/config", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { querystring: z.object({ @@ -240,8 +244,11 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/config", method: "POST", + url: "/config", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { body: z.object({ @@ -270,8 +277,11 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/config", method: "PATCH", + url: "/config", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { body: z diff --git a/backend/src/ee/routes/v1/scim-router.ts b/backend/src/ee/routes/v1/scim-router.ts index e3424b6a9..955c78c7f 100644 --- a/backend/src/ee/routes/v1/scim-router.ts +++ b/backend/src/ee/routes/v1/scim-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { ScimTokensSchema } from "@app/db/schemas"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -20,6 +21,9 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { server.route({ url: "/scim-tokens", method: "POST", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { body: z.object({ @@ -51,6 +55,9 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { server.route({ url: "/scim-tokens", method: "GET", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { querystring: z.object({ @@ -78,6 +85,9 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { server.route({ url: "/scim-tokens/:scimTokenId", method: "DELETE", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts index 1ea16178f..f6a955625 100644 --- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts @@ -1,6 +1,7 @@ import { nanoid } from "nanoid"; import { z } from "zod"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { sapPubSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -9,6 +10,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi server.route({ url: "/", method: "POST", + config: { + rateLimit: writeLimit + }, schema: { body: z .object({ @@ -47,6 +51,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi server.route({ url: "/:sapId", method: "PATCH", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ sapId: z.string() @@ -85,6 +92,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi server.route({ url: "/:sapId", method: "DELETE", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ sapId: z.string() @@ -111,6 +121,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi server.route({ url: "/", method: "GET", + config: { + rateLimit: readLimit + }, schema: { querystring: z.object({ workspaceId: z.string().trim() @@ -137,6 +150,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi server.route({ url: "/board", method: "GET", + config: { + rateLimit: readLimit + }, schema: { querystring: z.object({ workspaceId: z.string().trim(), diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index 16b6d205b..2a9cc405d 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -10,13 +10,17 @@ import { } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSecretApprovalRequestRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/", method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, schema: { querystring: z.object({ workspaceId: z.string().trim(), @@ -62,8 +66,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv }); server.route({ - url: "/count", method: "GET", + url: "/count", + config: { + rateLimit: readLimit + }, schema: { querystring: z.object({ workspaceId: z.string().trim() @@ -93,6 +100,9 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv server.route({ url: "/:id/merge", method: "POST", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ id: z.string() @@ -117,8 +127,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv }); server.route({ - url: "/:id/review", method: "POST", + url: "/:id/review", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ id: z.string() @@ -147,8 +160,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv }); server.route({ - url: "/:id/status", method: "POST", + url: "/:id/status", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ id: z.string() @@ -203,8 +219,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv .array() .optional(); server.route({ - url: "/:id", method: "GET", + url: "/:id", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ id: z.string() diff --git a/backend/src/ee/routes/v1/secret-rotation-provider-router.ts b/backend/src/ee/routes/v1/secret-rotation-provider-router.ts index 516885936..58419d3b7 100644 --- a/backend/src/ee/routes/v1/secret-rotation-provider-router.ts +++ b/backend/src/ee/routes/v1/secret-rotation-provider-router.ts @@ -1,12 +1,16 @@ import { z } from "zod"; +import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSecretRotationProviderRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/:workspaceId", method: "GET", + url: "/:workspaceId", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ workspaceId: z.string().trim() diff --git a/backend/src/ee/routes/v1/secret-rotation-router.ts b/backend/src/ee/routes/v1/secret-rotation-router.ts index 447280cf8..d951eb744 100644 --- a/backend/src/ee/routes/v1/secret-rotation-router.ts +++ b/backend/src/ee/routes/v1/secret-rotation-router.ts @@ -2,13 +2,17 @@ import { z } from "zod"; import { SecretRotationOutputsSchema, SecretRotationsSchema, SecretsSchema } from "@app/db/schemas"; import { removeTrailingSlash } from "@app/lib/fn"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSecretRotationRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/", method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ workspaceId: z.string().trim(), @@ -52,6 +56,9 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) = server.route({ url: "/restart", method: "POST", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ id: z.string().trim() @@ -86,6 +93,9 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) = server.route({ url: "/", method: "GET", + config: { + rateLimit: readLimit + }, schema: { querystring: z.object({ workspaceId: z.string().trim() @@ -136,8 +146,11 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) = }); server.route({ - url: "/:id", method: "DELETE", + url: "/:id", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ id: z.string().trim() diff --git a/backend/src/ee/routes/v1/secret-scanning-router.ts b/backend/src/ee/routes/v1/secret-scanning-router.ts index 1f8d56c74..2604d7232 100644 --- a/backend/src/ee/routes/v1/secret-scanning-router.ts +++ b/backend/src/ee/routes/v1/secret-scanning-router.ts @@ -2,13 +2,17 @@ import { z } from "zod"; import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas"; import { SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSecretScanningRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/create-installation-session/organization", method: "POST", + url: "/create-installation-session/organization", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ organizationId: z.string().trim() }), response: { @@ -31,8 +35,11 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = }); server.route({ - url: "/link-installation", method: "POST", + url: "/link-installation", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ installationId: z.string(), @@ -56,8 +63,11 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = }); server.route({ - url: "/installation-status/organization/:organizationId", method: "GET", + url: "/installation-status/organization/:organizationId", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), response: { @@ -80,6 +90,9 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = server.route({ url: "/organization/:organizationId/risks", method: "GET", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), response: { @@ -100,8 +113,11 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = }); server.route({ - url: "/organization/:organizationId/risks/:riskId/status", method: "POST", + url: "/organization/:organizationId/risks/:riskId/status", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim(), riskId: z.string().trim() }), body: z.object({ status: z.nativeEnum(SecretScanningRiskStatus) }), diff --git a/backend/src/ee/routes/v1/secret-version-router.ts b/backend/src/ee/routes/v1/secret-version-router.ts index 630af10cf..0604135ba 100644 --- a/backend/src/ee/routes/v1/secret-version-router.ts +++ b/backend/src/ee/routes/v1/secret-version-router.ts @@ -1,13 +1,17 @@ import { z } from "zod"; import { SecretVersionsSchema } from "@app/db/schemas"; +import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSecretVersionRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/:secretId/secret-versions", method: "GET", + url: "/:secretId/secret-versions", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ secretId: z.string() diff --git a/backend/src/ee/routes/v1/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts index 902d707af..7eee44f7e 100644 --- a/backend/src/ee/routes/v1/snapshot-router.ts +++ b/backend/src/ee/routes/v1/snapshot-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { SecretSnapshotsSchema, SecretTagsSchema, SecretVersionsSchema } from "@app/db/schemas"; import { PROJECTS } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -9,6 +10,9 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:secretSnapshotId", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ secretSnapshotId: z.string().trim() @@ -58,6 +62,9 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/:secretSnapshotId/rollback", + config: { + rateLimit: writeLimit + }, schema: { description: "Roll back project secrets to those captured in a secret snapshot version.", security: [ diff --git a/backend/src/ee/routes/v1/trusted-ip-router.ts b/backend/src/ee/routes/v1/trusted-ip-router.ts index 3d4ac8010..b6fc3cc90 100644 --- a/backend/src/ee/routes/v1/trusted-ip-router.ts +++ b/backend/src/ee/routes/v1/trusted-ip-router.ts @@ -2,13 +2,17 @@ import { z } from "zod"; import { TrustedIpsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/:workspaceId/trusted-ips", method: "GET", + url: "/:workspaceId/trusted-ips", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ workspaceId: z.string().trim() @@ -33,8 +37,11 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:workspaceId/trusted-ips", method: "POST", + url: "/:workspaceId/trusted-ips", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ workspaceId: z.string().trim() @@ -78,8 +85,11 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:workspaceId/trusted-ips/:trustedIpId", method: "PATCH", + url: "/:workspaceId/trusted-ips/:trustedIpId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ workspaceId: z.string().trim(), @@ -124,8 +134,11 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:workspaceId/trusted-ips/:trustedIpId", method: "DELETE", + url: "/:workspaceId/trusted-ips/:trustedIpId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ workspaceId: z.string().trim(), diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts index 9b6bfb6fb..7225caecf 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -6,6 +6,7 @@ import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -13,6 +14,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr server.route({ url: "/permanent", method: "POST", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId), @@ -21,11 +25,11 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr .min(1) .max(60) .trim() - .default(slugify(alphaNumericNanoId(12))) .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") .refine((v) => slugify(v) === v, { message: "Slug must be a valid slug" }) + .optional() .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions) }), @@ -43,6 +47,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, ...req.body, + slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), isTemporary: false, permissions: JSON.stringify(req.body.permissions) }); @@ -51,8 +56,11 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }); server.route({ - url: "/temporary", method: "POST", + url: "/temporary", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId), @@ -61,11 +69,11 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr .min(1) .max(60) .trim() - .default(`privilege-${slugify(alphaNumericNanoId(12))}`) .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") .refine((v) => slugify(v) === v, { message: "Slug must be a valid slug" }) + .optional() .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), temporaryMode: z @@ -94,6 +102,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, ...req.body, + slug: req.body.slug ? slugify(req.body.slug) : `privilege-${slugify(alphaNumericNanoId(12))}`, isTemporary: true, permissions: JSON.stringify(req.body.permissions) }); @@ -102,8 +111,11 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }); server.route({ - url: "/:privilegeId", method: "PATCH", + url: "/:privilegeId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ privilegeId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.privilegeId) @@ -156,8 +168,11 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }); server.route({ - url: "/:privilegeId", method: "DELETE", + url: "/:privilegeId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ privilegeId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.DELETE.privilegeId) @@ -182,8 +197,11 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }); server.route({ - url: "/", method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, schema: { querystring: z.object({ projectMembershipId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.LIST.projectMembershipId) @@ -208,8 +226,11 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }); server.route({ - url: "/:privilegeId", method: "GET", + url: "/:privilegeId", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ privilegeId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.GET_BY_PRIVILEGEID.privilegeId) diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 78b8ad8f2..eda19c215 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -5,9 +5,13 @@ import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { if (!actorAuthMethod) return false; - return [AuthMethod.AZURE_SAML, AuthMethod.OKTA_SAML, AuthMethod.JUMPCLOUD_SAML, AuthMethod.GOOGLE_SAML].includes( - actorAuthMethod - ); + return [ + AuthMethod.AZURE_SAML, + AuthMethod.OKTA_SAML, + AuthMethod.JUMPCLOUD_SAML, + AuthMethod.GOOGLE_SAML, + AuthMethod.KEYCLOAK_SAML + ].includes(actorAuthMethod); } function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) { diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index dc9728957..f88182e61 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -319,6 +319,11 @@ export const samlConfigServiceFactory = ({ const organization = await orgDAL.findOrgById(orgId); if (!organization) throw new BadRequestError({ message: "Org not found" }); + // TODO(dangtony98): remove this after aliases update + if (authProvider === AuthMethod.KEYCLOAK_SAML && appCfg.LICENSE_SERVER_KEY) { + throw new BadRequestError({ message: "Keycloak SAML is not yet available on Infisical Cloud" }); + } + if (user) { await userDAL.transaction(async (tx) => { const [orgMembership] = await orgDAL.findMembership( diff --git a/backend/src/ee/services/saml-config/saml-config-types.ts b/backend/src/ee/services/saml-config/saml-config-types.ts index 9aedf5d19..df7694920 100644 --- a/backend/src/ee/services/saml-config/saml-config-types.ts +++ b/backend/src/ee/services/saml-config/saml-config-types.ts @@ -5,7 +5,8 @@ export enum SamlProviders { OKTA_SAML = "okta-saml", AZURE_SAML = "azure-saml", JUMPCLOUD_SAML = "jumpcloud-saml", - GOOGLE_SAML = "google-saml" + GOOGLE_SAML = "google-saml", + KEYCLOAK_SAML = "keycloak-saml" } export type TCreateSamlCfgDTO = { diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 70ba7e87b..fd51119f1 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -215,6 +215,7 @@ export const SECRETS = { export const RAW_SECRETS = { LIST: { + recursive: "Whether or not to fetch all secrets from the specified base path, and all of its subdirectories.", workspaceId: "The ID of the project to list secrets from.", workspaceSlug: "The slug of the project to list secrets from. This parameter is only usable by machine identities.", environment: "The slug of the environment to list secrets from.", @@ -403,8 +404,11 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = { projectSlug: "The slug of the project of the identity in.", identityId: "The ID of the identity to delete.", slug: "The slug of the privilege to create.", - permissions: - "The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape", + permissions: `The permission object for the privilege. +1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]] +2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]] +2. [["read", "secrets", {environment: "dev"}]] +`, isPackPermission: "Whether the server should pack(compact) the permission object.", isTemporary: "Whether the privilege is temporary.", temporaryMode: "Type of temporary access given. Types: relative", @@ -417,7 +421,6 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = { slug: "The slug of the privilege to update.", newSlug: "The new slug of the privilege to update.", permissions: `The permission object for the privilege. -Example unpacked permission shape 1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]] 2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]] 2. [["read", "secrets", {environment: "dev"}]] @@ -479,3 +482,74 @@ export const PROJECT_USER_ADDITIONAL_PRIVILEGE = { projectMembershipId: "Project membership id of user" } }; + +export const INTEGRATION_AUTH = { + GET: { + integrationAuthId: "The id of integration authentication object." + }, + DELETE: { + integration: "The slug of the integration to be unauthorized.", + projectId: "The ID of the project to delete the integration auth from." + }, + DELETE_BY_ID: { + integrationAuthId: "The id of integration authentication object to delete." + }, + CREATE_ACCESS_TOKEN: { + workspaceId: "The ID of the project to create the integration auth for.", + integration: "The slug of integration for the auth object.", + accessId: "The unique authorized access id of the external integration provider.", + accessToken: "The unique authorized access token of the external integration provider.", + url: "", + namespace: "", + refreshToken: "The refresh token for integration authorization." + }, + LIST_AUTHORIZATION: { + workspaceId: "The ID of the project to list integration auths for." + } +}; + +export const INTEGRATION = { + CREATE: { + integrationAuthId: "The ID of the integration auth object to link with integration.", + app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.", + isActive: "Whether the integration should be active or disabled.", + appId: + "The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.", + secretPath: "The path of the secrets to sync secrets from.", + sourceEnvironment: "The environment to sync secret from.", + targetEnvironment: + "The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.", + targetEnvironmentId: + "The target environment id of the integration provider. Used in cloudflare pages, teamcity, gitlab integrations.", + targetService: + "The service based grouping identifier of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank", + targetServiceId: + "The service based grouping identifier ID of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank", + owner: "External integration providers service entity owner. Used in Github.", + path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault", + region: "AWS region to sync secrets to.", + scope: "Scope of the provider. Used by Github, Qovery", + metadata: { + 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", + shouldAutoRedeploy: "Used by Render to trigger auto deploy", + secretGCPLabel: "The label for the GCP secrets" + } + }, + UPDATE: { + integrationId: "The ID of the integration object.", + app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.", + appId: + "The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.", + isActive: "Whether the integration should be active or disabled.", + secretPath: "The path of the secrets to sync secrets from.", + owner: "External integration providers service entity owner. Used in Github.", + targetEnvironment: + "The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.", + environment: "The environment to sync secrets from." + }, + DELETE: { + integrationId: "The ID of the integration object." + } +}; diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts index 444158cbf..d8069b9db 100644 --- a/backend/src/server/config/rateLimiter.ts +++ b/backend/src/server/config/rateLimiter.ts @@ -18,14 +18,43 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => { }; }; -export const authRateLimit: RateLimitOptions = { +// GET endpoints +export const readLimit: RateLimitOptions = { timeWindow: 60 * 1000, max: 600, keyGenerator: (req) => req.realIp }; -export const passwordRateLimit: RateLimitOptions = { +// POST, PATCH, PUT, DELETE endpoints +export const writeLimit: RateLimitOptions = { + timeWindow: 60 * 1000, + max: 50, + keyGenerator: (req) => req.realIp +}; + +// special endpoints +export const secretsLimit: RateLimitOptions = { + // secrets, folders, secret imports timeWindow: 60 * 1000, max: 600, keyGenerator: (req) => req.realIp }; + +export const authRateLimit: RateLimitOptions = { + timeWindow: 60 * 1000, + max: 60, + keyGenerator: (req) => req.realIp +}; + +export const inviteUserRateLimit: RateLimitOptions = { + timeWindow: 60 * 1000, + max: 30, + keyGenerator: (req) => req.realIp +}; + +export const creationLimit: RateLimitOptions = { + // identity, project, org + timeWindow: 60 * 1000, + max: 30, + keyGenerator: (req) => req.realIp +}; diff --git a/backend/src/server/plugins/secret-scanner.ts b/backend/src/server/plugins/secret-scanner.ts index 8790d54d4..d20008de7 100644 --- a/backend/src/server/plugins/secret-scanner.ts +++ b/backend/src/server/plugins/secret-scanner.ts @@ -4,6 +4,7 @@ import SmeeClient from "smee-client"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; +import { writeLimit } from "@app/server/config/rateLimiter"; export const registerSecretScannerGhApp = async (server: FastifyZodProvider) => { const probotApp = (app: Probot) => { @@ -49,6 +50,9 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) => server.route({ method: "POST", url: "/", + config: { + rateLimit: writeLimit + }, handler: async (req, res) => { const eventName = req.headers["x-github-event"]; const signatureSHA256 = req.headers["x-hub-signature-256"] as string; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 7745a8a04..d94d00ccb 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -49,6 +49,7 @@ import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip- import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { TQueueServiceFactory } from "@app/queue"; +import { readLimit } from "@app/server/config/rateLimiter"; import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal"; import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { authDALFactory } from "@app/services/auth/auth-dal"; @@ -398,7 +399,8 @@ export const registerRoutes = async ( folderDAL, licenseService, projectUserMembershipRoleDAL, - identityProjectMembershipRoleDAL + identityProjectMembershipRoleDAL, + keyStore }); const projectEnvService = projectEnvServiceFactory({ @@ -409,7 +411,12 @@ export const registerRoutes = async ( folderDAL }); - const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDAL }); + const projectRoleService = projectRoleServiceFactory({ + permissionService, + projectRoleDAL, + projectUserMembershipRoleDAL, + identityProjectMembershipRoleDAL + }); const snapshotService = secretSnapshotServiceFactory({ permissionService, @@ -490,6 +497,7 @@ export const registerRoutes = async ( snapshotService, secretQueueService, secretImportDAL, + projectEnvDAL, projectBotService }); const sarService = secretApprovalRequestServiceFactory({ @@ -669,8 +677,11 @@ export const registerRoutes = async ( await server.register(injectAuditLogInfo); server.route({ - url: "/api/status", method: "GET", + url: "/api/status", + config: { + rateLimit: readLimit + }, schema: { response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 730ba6cae..e70822128 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { UnauthorizedError } from "@app/lib/errors"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -11,8 +12,11 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; export const registerAdminRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/config", method: "GET", + url: "/config", + config: { + rateLimit: readLimit + }, schema: { response: { 200: z.object({ @@ -30,8 +34,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/config", method: "PATCH", + url: "/config", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ allowSignUp: z.boolean().optional(), @@ -55,8 +62,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/signup", method: "POST", + url: "/signup", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ email: z.string().email().trim(), diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index 16448b8ca..7f09a904b 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; -import { authRateLimit } from "@app/server/config/rateLimiter"; +import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; @@ -38,8 +38,11 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { }); server.route({ - url: "/checkAuth", method: "POST", + url: "/checkAuth", + config: { + rateLimit: writeLimit + }, schema: { response: { 200: z.object({ @@ -52,8 +55,11 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { }); server.route({ - url: "/token", method: "POST", + url: "/token", + config: { + rateLimit: writeLimit + }, schema: { response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/bot-router.ts b/backend/src/server/routes/v1/bot-router.ts index 7e1d6ec94..34a34f843 100644 --- a/backend/src/server/routes/v1/bot-router.ts +++ b/backend/src/server/routes/v1/bot-router.ts @@ -1,13 +1,17 @@ import { z } from "zod"; import { ProjectBotsSchema } from "@app/db/schemas"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerProjectBotRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/:projectId", method: "GET", + url: "/:projectId", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ projectId: z.string().trim() @@ -38,8 +42,11 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:botId/active", method: "PATCH", + url: "/:botId/active", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ isActive: z.boolean(), diff --git a/backend/src/server/routes/v1/identity-access-token-router.ts b/backend/src/server/routes/v1/identity-access-token-router.ts index 5cd0b27a1..387c54c13 100644 --- a/backend/src/server/routes/v1/identity-access-token-router.ts +++ b/backend/src/server/routes/v1/identity-access-token-router.ts @@ -1,11 +1,15 @@ import { z } from "zod"; import { UNIVERSAL_AUTH } from "@app/lib/api-docs"; +import { writeLimit } from "@app/server/config/rateLimiter"; export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvider) => { server.route({ url: "/token/renew", method: "POST", + config: { + rateLimit: writeLimit + }, schema: { description: "Renew access token", body: z.object({ diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index 3ce13ca0a..e174cf974 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { IdentitiesSchema, OrgMembershipRole } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { IDENTITIES } from "@app/lib/api-docs"; +import { creationLimit, 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"; @@ -12,6 +13,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/", + config: { + rateLimit: creationLimit + }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Create identity", @@ -71,6 +75,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", url: "/:identityId", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Update identity", @@ -121,6 +128,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", url: "/:identityId", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Delete identity", diff --git a/backend/src/server/routes/v1/identity-ua.ts b/backend/src/server/routes/v1/identity-ua.ts index ac5c78a08..670f52416 100644 --- a/backend/src/server/routes/v1/identity-ua.ts +++ b/backend/src/server/routes/v1/identity-ua.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { IdentityUaClientSecretsSchema, IdentityUniversalAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { UNIVERSAL_AUTH } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; @@ -22,8 +23,11 @@ export const sanitizedClientSecretSchema = IdentityUaClientSecretsSchema.pick({ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/universal-auth/login", method: "POST", + url: "/universal-auth/login", + config: { + rateLimit: writeLimit + }, schema: { description: "Login with Universal Auth", body: z.object({ @@ -66,8 +70,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/universal-auth/identities/:identityId", method: "POST", + url: "/universal-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Attach Universal Auth configuration onto identity", @@ -156,8 +163,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/universal-auth/identities/:identityId", method: "PATCH", + url: "/universal-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Update Universal Auth configuration on identity", @@ -239,8 +249,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/universal-auth/identities/:identityId", method: "GET", + url: "/universal-auth/identities/:identityId", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Retrieve Universal Auth configuration on identity", @@ -283,8 +296,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/universal-auth/identities/:identityId/client-secrets", method: "POST", + url: "/universal-auth/identities/:identityId/client-secrets", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Create Universal Auth Client Secret for identity", @@ -335,8 +351,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/universal-auth/identities/:identityId/client-secrets", method: "GET", + url: "/universal-auth/identities/:identityId/client-secrets", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "List Universal Auth Client Secrets for identity", @@ -378,8 +397,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/universal-auth/identities/:identityId/client-secrets/:clientSecretId/revoke", method: "POST", + url: "/universal-auth/identities/:identityId/client-secrets/:clientSecretId/revoke", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Revoke Universal Auth Client Secrets for identity", diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index 004ca298d..604ac0bc2 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -1,6 +1,8 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { INTEGRATION_AUTH } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -8,10 +10,19 @@ import { integrationAuthPubSchema } from "../sanitizedSchemas"; export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/integration-options", method: "GET", - onRequest: verifyAuth([AuthMode.JWT]), + url: "/integration-options", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + description: "List of integrations available.", + security: [ + { + bearerAuth: [] + } + ], response: { 200: z.object({ integrationOptions: z @@ -36,12 +47,21 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId", method: "GET", - onRequest: verifyAuth([AuthMode.JWT]), + url: "/:integrationAuthId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + description: "Get details of an integration authorization by auth object id.", + security: [ + { + bearerAuth: [] + } + ], params: z.object({ - integrationAuthId: z.string().trim() + integrationAuthId: z.string().trim().describe(INTEGRATION_AUTH.GET.integrationAuthId) }), response: { 200: z.object({ @@ -62,13 +82,22 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/", method: "DELETE", - onRequest: verifyAuth([AuthMode.JWT]), + url: "/", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + description: "Remove all integration's auth object from the project.", + security: [ + { + bearerAuth: [] + } + ], querystring: z.object({ - integration: z.string().trim(), - projectId: z.string().trim() + integration: z.string().trim().describe(INTEGRATION_AUTH.DELETE.integration), + projectId: z.string().trim().describe(INTEGRATION_AUTH.DELETE.projectId) }), response: { 200: z.object({ @@ -102,12 +131,21 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId", method: "DELETE", - onRequest: verifyAuth([AuthMode.JWT]), + url: "/:integrationAuthId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + description: "Remove an integration auth object by object id.", + security: [ + { + bearerAuth: [] + } + ], params: z.object({ - integrationAuthId: z.string().trim() + integrationAuthId: z.string().trim().describe(INTEGRATION_AUTH.DELETE_BY_ID.integrationAuthId) }), response: { 200: z.object({ @@ -140,8 +178,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/oauth-token", method: "POST", + url: "/oauth-token", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { body: z.object({ @@ -181,18 +222,27 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/access-token", method: "POST", - onRequest: verifyAuth([AuthMode.JWT]), + url: "/access-token", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + description: "Create the integration authentication object required for syncing secrets.", + security: [ + { + bearerAuth: [] + } + ], body: z.object({ - workspaceId: z.string().trim(), - integration: z.string().trim(), - accessId: z.string().trim().optional(), - accessToken: z.string().trim().optional(), - url: z.string().url().trim().optional(), - namespace: z.string().trim().optional(), - refreshToken: z.string().trim().optional() + workspaceId: z.string().trim().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.workspaceId), + integration: z.string().trim().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.integration), + accessId: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessId), + accessToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessToken), + url: z.string().url().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.url), + namespace: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.namespace), + refreshToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.refreshToken) }), response: { 200: z.object({ @@ -225,8 +275,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/apps", method: "GET", + url: "/:integrationAuthId/apps", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -262,8 +315,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/teams", method: "GET", + url: "/:integrationAuthId/teams", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -293,8 +349,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/vercel/branches", method: "GET", + url: "/:integrationAuthId/vercel/branches", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -323,8 +382,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/checkly/groups", method: "GET", + url: "/:integrationAuthId/checkly/groups", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -353,8 +415,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/github/orgs", method: "GET", + url: "/:integrationAuthId/github/orgs", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -381,8 +446,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/github/envs", method: "GET", + url: "/:integrationAuthId/github/envs", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -415,8 +483,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/qovery/orgs", method: "GET", + url: "/:integrationAuthId/qovery/orgs", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -441,8 +512,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/qovery/projects", method: "GET", + url: "/:integrationAuthId/qovery/projects", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -471,8 +545,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/qovery/environments", method: "GET", + url: "/:integrationAuthId/qovery/environments", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -501,8 +578,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/qovery/apps", method: "GET", + url: "/:integrationAuthId/qovery/apps", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -531,8 +611,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/qovery/containers", method: "GET", + url: "/:integrationAuthId/qovery/containers", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -561,8 +644,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/qovery/jobs", method: "GET", + url: "/:integrationAuthId/qovery/jobs", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -591,8 +677,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/heroku/pipelines", method: "GET", + url: "/:integrationAuthId/heroku/pipelines", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -623,8 +712,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/railway/environments", method: "GET", + url: "/:integrationAuthId/railway/environments", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -653,8 +745,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/railway/services", method: "GET", + url: "/:integrationAuthId/railway/services", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -683,8 +778,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/bitbucket/workspaces", method: "GET", + url: "/:integrationAuthId/bitbucket/workspaces", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -719,8 +817,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/northflank/secret-groups", method: "GET", + url: "/:integrationAuthId/northflank/secret-groups", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -754,8 +855,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:integrationAuthId/teamcity/build-configs", method: "GET", + url: "/:integrationAuthId/teamcity/build-configs", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index 4859ca584..cf8fbf41f 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -2,7 +2,9 @@ import { z } from "zod"; import { IntegrationsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { INTEGRATION } from "@app/lib/api-docs"; import { removeTrailingSlash, shake } from "@app/lib/fn"; +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"; @@ -10,36 +12,51 @@ import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telem export const registerIntegrationRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/", method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, schema: { + description: "Create an integration to sync secrets.", + security: [ + { + bearerAuth: [] + } + ], body: z.object({ - integrationAuthId: z.string().trim(), - app: z.string().trim().optional(), - isActive: z.boolean(), - appId: z.string().trim().optional(), - secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - sourceEnvironment: z.string().trim(), - targetEnvironment: z.string().trim().optional(), - targetEnvironmentId: z.string().trim().optional(), - targetService: z.string().trim().optional(), - targetServiceId: z.string().trim().optional(), - owner: z.string().trim().optional(), - path: z.string().trim().optional(), - region: z.string().trim().optional(), - scope: z.string().trim().optional(), + integrationAuthId: z.string().trim().describe(INTEGRATION.CREATE.integrationAuthId), + app: z.string().trim().optional().describe(INTEGRATION.CREATE.app), + isActive: z.boolean().describe(INTEGRATION.CREATE.isActive).default(true), + appId: z.string().trim().optional().describe(INTEGRATION.CREATE.appId), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(INTEGRATION.CREATE.secretPath), + sourceEnvironment: z.string().trim().describe(INTEGRATION.CREATE.sourceEnvironment), + targetEnvironment: z.string().trim().optional().describe(INTEGRATION.CREATE.targetEnvironment), + targetEnvironmentId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetEnvironmentId), + targetService: z.string().trim().optional().describe(INTEGRATION.CREATE.targetService), + targetServiceId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetServiceId), + owner: z.string().trim().optional().describe(INTEGRATION.CREATE.owner), + path: z.string().trim().optional().describe(INTEGRATION.CREATE.path), + region: z.string().trim().optional().describe(INTEGRATION.CREATE.region), + scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope), metadata: z .object({ - secretPrefix: z.string().optional(), - secretSuffix: z.string().optional(), - initialSyncBehavior: z.string().optional(), - shouldAutoRedeploy: z.boolean().optional(), + 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), + shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy), secretGCPLabel: z .object({ labelName: z.string(), labelValue: z.string() }) .optional() + .describe(INTEGRATION.CREATE.metadata.secretGCPLabel) }) .optional() }), @@ -49,7 +66,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const { integration, integrationAuth } = await server.services.integration.createIntegration({ actorId: req.permission.id, @@ -99,20 +116,34 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:integrationId", method: "PATCH", + url: "/:integrationId", + config: { + rateLimit: writeLimit + }, schema: { + description: "Update an integration by integration id", + security: [ + { + bearerAuth: [] + } + ], params: z.object({ - integrationId: z.string().trim() + integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId) }), body: z.object({ - app: z.string().trim(), - appId: z.string().trim(), - isActive: z.boolean(), - secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - targetEnvironment: z.string().trim(), - owner: z.string().trim(), - environment: z.string().trim() + app: z.string().trim().describe(INTEGRATION.UPDATE.app), + appId: z.string().trim().describe(INTEGRATION.UPDATE.appId), + isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(INTEGRATION.UPDATE.secretPath), + targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment), + owner: z.string().trim().describe(INTEGRATION.UPDATE.owner), + environment: z.string().trim().describe(INTEGRATION.UPDATE.environment) }), response: { 200: z.object({ @@ -120,7 +151,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const integration = await server.services.integration.updateIntegration({ actorId: req.permission.id, @@ -135,11 +166,20 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:integrationId", method: "DELETE", + url: "/:integrationId", + config: { + rateLimit: writeLimit + }, schema: { + description: "Remove an integration using the integration object ID", + security: [ + { + bearerAuth: [] + } + ], params: z.object({ - integrationId: z.string().trim() + integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId) }), response: { 200: z.object({ @@ -147,7 +187,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const integration = await server.services.integration.deleteIntegration({ actorId: req.permission.id, diff --git a/backend/src/server/routes/v1/invite-org-router.ts b/backend/src/server/routes/v1/invite-org-router.ts index 212020034..873710f10 100644 --- a/backend/src/server/routes/v1/invite-org-router.ts +++ b/backend/src/server/routes/v1/invite-org-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { UsersSchema } from "@app/db/schemas"; +import { inviteUserRateLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; @@ -9,6 +10,9 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; export const registerInviteOrgRouter = async (server: FastifyZodProvider) => { server.route({ url: "/signup", + config: { + rateLimit: inviteUserRateLimit + }, method: "POST", schema: { body: z.object({ @@ -52,6 +56,9 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => { server.route({ url: "/verify", method: "POST", + config: { + rateLimit: inviteUserRateLimit + }, schema: { body: z.object({ email: z.string().trim().email(), diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 42e64e9d7..651b76bac 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { IncidentContactsSchema, OrganizationsSchema, OrgMembershipsSchema, UsersSchema } from "@app/db/schemas"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -8,6 +9,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/", + config: { + rateLimit: readLimit + }, schema: { response: { 200: z.object({ @@ -25,6 +29,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:organizationId", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() @@ -50,6 +57,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:organizationId/users", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() @@ -87,6 +97,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", url: "/:organizationId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), body: z.object({ @@ -128,6 +141,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:organizationId/incidentContactOrg", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), response: { @@ -151,6 +167,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/:organizationId/incidentContactOrg", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim() }), body: z.object({ email: z.string().email().trim() }), @@ -176,6 +195,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", url: "/:organizationId/incidentContactOrg/:incidentContactId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim(), incidentContactId: z.string().trim() }), response: { diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts index d5c5054df..a8ef3fb77 100644 --- a/backend/src/server/routes/v1/password-router.ts +++ b/backend/src/server/routes/v1/password-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { BackupPrivateKeySchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -import { passwordRateLimit } from "@app/server/config/rateLimiter"; +import { authRateLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { validateSignUpAuthorization } from "@app/services/auth/auth-fns"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -12,7 +12,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { method: "POST", url: "/srp1", config: { - rateLimit: passwordRateLimit + rateLimit: authRateLimit }, schema: { body: z.object({ @@ -39,7 +39,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { method: "POST", url: "/change-password", config: { - rateLimit: passwordRateLimit + rateLimit: authRateLimit }, schema: { body: z.object({ @@ -78,7 +78,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { method: "POST", url: "/email/password-reset", config: { - rateLimit: passwordRateLimit + rateLimit: authRateLimit }, schema: { body: z.object({ @@ -103,7 +103,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { method: "POST", url: "/email/password-reset-verify", config: { - rateLimit: passwordRateLimit + rateLimit: authRateLimit }, schema: { body: z.object({ @@ -133,7 +133,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { method: "POST", url: "/backup-private-key", config: { - rateLimit: passwordRateLimit + rateLimit: authRateLimit }, onRequest: verifyAuth([AuthMode.JWT]), schema: { @@ -168,7 +168,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { method: "GET", url: "/backup-private-key", config: { - rateLimit: passwordRateLimit + rateLimit: authRateLimit }, schema: { response: { @@ -190,6 +190,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/password-reset", + config: { + rateLimit: authRateLimit + }, schema: { body: z.object({ protectedKey: z.string().trim(), diff --git a/backend/src/server/routes/v1/project-env-router.ts b/backend/src/server/routes/v1/project-env-router.ts index 68a5a9001..e1efa1637 100644 --- a/backend/src/server/routes/v1/project-env-router.ts +++ b/backend/src/server/routes/v1/project-env-router.ts @@ -3,13 +3,17 @@ import { z } from "zod"; import { ProjectEnvironmentsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ENVIRONMENTS } from "@app/lib/api-docs"; +import { writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/:workspaceId/environments", method: "POST", + url: "/:workspaceId/environments", + config: { + rateLimit: writeLimit + }, schema: { description: "Create environment", security: [ @@ -64,8 +68,11 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:workspaceId/environments/:id", method: "PATCH", + url: "/:workspaceId/environments/:id", + config: { + rateLimit: writeLimit + }, schema: { description: "Update environment", security: [ @@ -128,8 +135,11 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:workspaceId/environments/:id", method: "DELETE", + url: "/:workspaceId/environments/:id", + config: { + rateLimit: writeLimit + }, schema: { description: "Delete environment", security: [ diff --git a/backend/src/server/routes/v1/project-key-router.ts b/backend/src/server/routes/v1/project-key-router.ts index 440d45140..bb35794e9 100644 --- a/backend/src/server/routes/v1/project-key-router.ts +++ b/backend/src/server/routes/v1/project-key-router.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -7,6 +8,9 @@ export const registerProjectKeyRouter = async (server: FastifyZodProvider) => { server.route({ url: "/:workspaceId/key", method: "POST", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ workspaceId: z.string().trim() diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index f277e556c..2b281e6fe 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -10,14 +10,18 @@ import { } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { PROJECTS } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/:workspaceId/memberships", method: "GET", + url: "/:workspaceId/memberships", + config: { + rateLimit: readLimit + }, schema: { description: "Return project user memberships", security: [ @@ -75,8 +79,11 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }); server.route({ - url: "/:workspaceId/memberships", method: "POST", + url: "/:workspaceId/memberships", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ workspaceId: z.string().trim() @@ -126,8 +133,11 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }); server.route({ - url: "/:workspaceId/memberships/:membershipId", method: "PATCH", + url: "/:workspaceId/memberships/:membershipId", + config: { + rateLimit: writeLimit + }, schema: { description: "Update project user membership", security: [ @@ -197,8 +207,11 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }); server.route({ - url: "/:workspaceId/memberships/:membershipId", method: "DELETE", + url: "/:workspaceId/memberships/:membershipId", + config: { + rateLimit: writeLimit + }, schema: { description: "Delete project user membership", security: [ diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 55f1fdda9..a4df0416e 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -7,7 +7,8 @@ import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; -import { PROJECTS } from "@app/lib/api-docs"; +import { INTEGRATION_AUTH, PROJECTS } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { ProjectFilterType } from "@app/services/project/project-types"; @@ -24,8 +25,11 @@ const projectWithEnv = ProjectsSchema.merge( export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/:workspaceId/keys", method: "GET", + url: "/:workspaceId/keys", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ workspaceId: z.string().trim() @@ -55,8 +59,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:workspaceId/users", method: "GET", + url: "/:workspaceId/users", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ workspaceId: z.string().trim() @@ -108,8 +115,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/", method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, schema: { response: { 200: z.object({ @@ -125,8 +135,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:workspaceId", method: "GET", + url: "/:workspaceId", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ workspaceId: z.string().trim().describe(PROJECTS.GET.workspaceId) @@ -154,8 +167,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:workspaceId", method: "DELETE", + url: "/:workspaceId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ workspaceId: z.string().trim().describe(PROJECTS.DELETE.workspaceId) @@ -185,6 +201,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ url: "/:workspaceId/name", method: "POST", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ workspaceId: z.string().trim() @@ -217,8 +236,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:workspaceId", method: "PATCH", + url: "/:workspaceId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ workspaceId: z.string().trim().describe(PROJECTS.UPDATE.workspaceId) @@ -261,8 +283,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:workspaceId/auto-capitalization", method: "POST", + url: "/:workspaceId/auto-capitalization", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ workspaceId: z.string().trim() @@ -295,8 +320,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:workspaceId/integrations", method: "GET", + url: "/:workspaceId/integrations", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ workspaceId: z.string().trim() @@ -329,11 +357,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:workspaceId/authorizations", method: "GET", + url: "/:workspaceId/authorizations", + config: { + rateLimit: readLimit + }, schema: { + description: "List integration auth objects for a workspace.", + security: [ + { + bearerAuth: [] + } + ], params: z.object({ - workspaceId: z.string().trim() + workspaceId: z.string().trim().describe(INTEGRATION_AUTH.LIST_AUTHORIZATION.workspaceId) }), response: { 200: z.object({ @@ -341,7 +378,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const authorizations = await server.services.integrationAuth.listIntegrationAuthByProjectId({ actorId: req.permission.id, @@ -355,8 +392,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:workspaceId/service-token-data", method: "GET", + url: "/:workspaceId/service-token-data", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ workspaceId: z.string().trim() diff --git a/backend/src/server/routes/v1/secret-folder-router.ts b/backend/src/server/routes/v1/secret-folder-router.ts index bd202b70b..36544de1c 100644 --- a/backend/src/server/routes/v1/secret-folder-router.ts +++ b/backend/src/server/routes/v1/secret-folder-router.ts @@ -4,6 +4,7 @@ import { SecretFoldersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { FOLDERS } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; +import { readLimit, secretsLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -11,6 +12,9 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => server.route({ url: "/", method: "POST", + config: { + rateLimit: secretsLimit + }, schema: { description: "Create folders", security: [ @@ -65,6 +69,9 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => server.route({ url: "/:folderId", method: "PATCH", + config: { + rateLimit: secretsLimit + }, schema: { description: "Update folder", security: [ @@ -124,8 +131,11 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => // TODO(daniel): Expose this route in api reference and write docs for it. server.route({ - url: "/:folderIdOrName", method: "DELETE", + url: "/:folderIdOrName", + config: { + rateLimit: secretsLimit + }, schema: { description: "Delete a folder", security: [ @@ -181,8 +191,11 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }); server.route({ - url: "/", method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, schema: { description: "Get folders", security: [ diff --git a/backend/src/server/routes/v1/secret-import-router.ts b/backend/src/server/routes/v1/secret-import-router.ts index bd47a7424..0d3f547b3 100644 --- a/backend/src/server/routes/v1/secret-import-router.ts +++ b/backend/src/server/routes/v1/secret-import-router.ts @@ -4,13 +4,17 @@ import { SecretImportsSchema, SecretsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { SECRET_IMPORTS } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; +import { readLimit, secretsLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSecretImportRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/", method: "POST", + url: "/", + config: { + rateLimit: secretsLimit + }, schema: { description: "Create secret imports", security: [ @@ -71,8 +75,11 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => }); server.route({ - url: "/:secretImportId", method: "PATCH", + url: "/:secretImportId", + config: { + rateLimit: secretsLimit + }, schema: { description: "Update secret imports", security: [ @@ -143,8 +150,11 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => }); server.route({ - url: "/:secretImportId", method: "DELETE", + url: "/:secretImportId", + config: { + rateLimit: secretsLimit + }, schema: { description: "Delete secret imports", security: [ @@ -204,8 +214,11 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => }); server.route({ - url: "/", method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, schema: { description: "Get secret imports", security: [ @@ -262,6 +275,9 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => server.route({ url: "/secrets", method: "GET", + config: { + rateLimit: secretsLimit + }, schema: { querystring: z.object({ workspaceId: z.string().trim(), diff --git a/backend/src/server/routes/v1/secret-tag-router.ts b/backend/src/server/routes/v1/secret-tag-router.ts index 519b257ae..1715aa3c3 100644 --- a/backend/src/server/routes/v1/secret-tag-router.ts +++ b/backend/src/server/routes/v1/secret-tag-router.ts @@ -2,13 +2,17 @@ import { z } from "zod"; import { SecretTagsSchema } from "@app/db/schemas"; import { SECRET_TAGS } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSecretTagRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/:projectId/tags", method: "GET", + url: "/:projectId/tags", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.LIST.projectId) @@ -33,8 +37,11 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:projectId/tags", method: "POST", + url: "/:projectId/tags", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId) @@ -65,8 +72,11 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:projectId/tags/:tagId", method: "DELETE", + url: "/:projectId/tags/:tagId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.DELETE.projectId), diff --git a/backend/src/server/routes/v1/user-action-router.ts b/backend/src/server/routes/v1/user-action-router.ts index c730cdb91..5a2ae484e 100644 --- a/backend/src/server/routes/v1/user-action-router.ts +++ b/backend/src/server/routes/v1/user-action-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { UserActionsSchema } from "@app/db/schemas"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -8,6 +9,9 @@ export const registerUserActionRouter = async (server: FastifyZodProvider) => { server.route({ url: "/", method: "POST", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ action: z.string().trim() @@ -29,6 +33,9 @@ export const registerUserActionRouter = async (server: FastifyZodProvider) => { server.route({ url: "/", method: "GET", + config: { + rateLimit: readLimit + }, schema: { querystring: z.object({ action: z.string().trim() diff --git a/backend/src/server/routes/v1/user-router.ts b/backend/src/server/routes/v1/user-router.ts index 031bcc941..bdede8a3a 100644 --- a/backend/src/server/routes/v1/user-router.ts +++ b/backend/src/server/routes/v1/user-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; +import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -8,6 +9,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/", + config: { + rateLimit: readLimit + }, schema: { response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/webhook-router.ts b/backend/src/server/routes/v1/webhook-router.ts index 2a5ab49fb..1698c0c4b 100644 --- a/backend/src/server/routes/v1/webhook-router.ts +++ b/backend/src/server/routes/v1/webhook-router.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { WebhooksSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { removeTrailingSlash } from "@app/lib/fn"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -27,6 +28,9 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { body: z.object({ @@ -75,6 +79,9 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", url: "/:webhookId", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -122,6 +129,9 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", url: "/:webhookId", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -159,6 +169,9 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/:webhookId/test", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ @@ -186,6 +199,9 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { querystring: z.object({ diff --git a/backend/src/server/routes/v2/identity-org-router.ts b/backend/src/server/routes/v2/identity-org-router.ts index 440130e13..671cca0a1 100644 --- a/backend/src/server/routes/v2/identity-org-router.ts +++ b/backend/src/server/routes/v2/identity-org-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; import { ORGANIZATIONS } from "@app/lib/api-docs"; +import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -9,6 +10,9 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:orgId/identity-memberships", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Return organization identity memberships", diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v2/identity-project-router.ts index d170f87e6..a4068053f 100644 --- a/backend/src/server/routes/v2/identity-project-router.ts +++ b/backend/src/server/routes/v2/identity-project-router.ts @@ -8,6 +8,7 @@ import { ProjectUserMembershipRolesSchema } from "@app/db/schemas"; import { PROJECTS } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; @@ -16,6 +17,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) server.route({ method: "POST", url: "/:projectId/identity-memberships/:identityId", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { params: z.object({ @@ -48,6 +52,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) server.route({ method: "PATCH", url: "/:projectId/identity-memberships/:identityId", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Update project identity memberships", @@ -103,6 +110,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) server.route({ method: "DELETE", url: "/:projectId/identity-memberships/:identityId", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Delete project identity memberships", @@ -137,6 +147,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) server.route({ method: "GET", url: "/:projectId/identity-memberships", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Return project identity memberships", diff --git a/backend/src/server/routes/v2/mfa-router.ts b/backend/src/server/routes/v2/mfa-router.ts index d8e46d0da..973804c7c 100644 --- a/backend/src/server/routes/v2/mfa-router.ts +++ b/backend/src/server/routes/v2/mfa-router.ts @@ -2,6 +2,7 @@ import jwt from "jsonwebtoken"; import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; +import { writeLimit } from "@app/server/config/rateLimiter"; import { AuthModeMfaJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; export const registerMfaRouter = async (server: FastifyZodProvider) => { @@ -30,8 +31,11 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/mfa/send", method: "POST", + url: "/mfa/send", + config: { + rateLimit: writeLimit + }, schema: { response: { 200: z.object({ @@ -48,6 +52,9 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => { server.route({ url: "/mfa/verify", method: "POST", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ mfaToken: z.string().trim() diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 83ab6792e..a4a66e992 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { OrganizationsSchema, OrgMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { ORGANIZATIONS } from "@app/lib/api-docs"; +import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; @@ -9,6 +10,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:organizationId/memberships", + config: { + rateLimit: readLimit + }, schema: { description: "Return organization user memberships", security: [ @@ -55,6 +59,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:organizationId/workspaces", + config: { + rateLimit: readLimit + }, schema: { description: "Return projects in organization that user is part of", security: [ @@ -101,6 +108,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", url: "/:organizationId/memberships/:membershipId", + config: { + rateLimit: writeLimit + }, schema: { description: "Update organization user memberships", security: [ @@ -141,6 +151,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", url: "/:organizationId/memberships/:membershipId", + config: { + rateLimit: writeLimit + }, schema: { description: "Delete organization user memberships", security: [ @@ -177,6 +190,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/", + config: { + rateLimit: creationLimit + }, schema: { body: z.object({ name: z.string().trim() @@ -204,6 +220,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", url: "/:organizationId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ organizationId: z.string().trim() diff --git a/backend/src/server/routes/v2/project-membership-router.ts b/backend/src/server/routes/v2/project-membership-router.ts index 51bae6d2b..cb9aa73a9 100644 --- a/backend/src/server/routes/v2/project-membership-router.ts +++ b/backend/src/server/routes/v2/project-membership-router.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { ProjectMembershipsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { PROJECTS } from "@app/lib/api-docs"; +import { writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -10,6 +11,9 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider server.route({ method: "POST", url: "/:projectId/memberships", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ projectId: z.string().describe(PROJECTS.INVITE_MEMBER.projectId) @@ -56,6 +60,9 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider server.route({ method: "DELETE", url: "/:projectId/memberships", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ projectId: z.string().describe(PROJECTS.REMOVE_MEMBER.projectId) diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 3258be50a..26c7cf30c 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { PROJECTS } from "@app/lib/api-docs"; -import { authRateLimit } from "@app/server/config/rateLimiter"; +import { creationLimit, readLimit, 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"; @@ -29,8 +29,11 @@ const slugSchema = z export const registerProjectRouter = async (server: FastifyZodProvider) => { /* Get project key */ server.route({ - url: "/:workspaceId/encrypted-key", method: "GET", + url: "/:workspaceId/encrypted-key", + config: { + rateLimit: readLimit + }, schema: { description: "Return encrypted project key", security: [ @@ -78,8 +81,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { /* Start upgrade of a project */ server.route({ - url: "/:projectId/upgrade", method: "POST", + url: "/:projectId/upgrade", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ projectId: z.string().trim() @@ -108,6 +114,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ url: "/:projectId/upgrade/status", method: "GET", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ projectId: z.string().trim() @@ -137,7 +146,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { method: "POST", url: "/", config: { - rateLimit: authRateLimit + rateLimit: creationLimit }, schema: { body: z.object({ @@ -187,6 +196,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", url: "/:slug", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ slug: slugSchema.describe("The slug of the project to delete.") @@ -218,6 +230,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/:slug", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ slug: slugSchema.describe("The slug of the project to get.") @@ -248,6 +263,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", url: "/:slug", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ slug: slugSchema.describe("The slug of the project to update.") diff --git a/backend/src/server/routes/v2/service-token-router.ts b/backend/src/server/routes/v2/service-token-router.ts index c4d08d104..fb10f17db 100644 --- a/backend/src/server/routes/v2/service-token-router.ts +++ b/backend/src/server/routes/v2/service-token-router.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { ServiceTokensSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { removeTrailingSlash } from "@app/lib/fn"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -17,8 +18,11 @@ export const sanitizedServiceTokenSchema = ServiceTokensSchema.omit({ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/", method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, onRequest: verifyAuth([AuthMode.SERVICE_TOKEN]), schema: { description: "Return Infisical Token data", @@ -69,8 +73,11 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => }); server.route({ - url: "/", method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { body: z.object({ @@ -122,8 +129,11 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => }); server.route({ - url: "/:serviceTokenId", method: "DELETE", + url: "/:serviceTokenId", + config: { + rateLimit: writeLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts index 12c9b703d..217e32ddf 100644 --- a/backend/src/server/routes/v2/user-router.ts +++ b/backend/src/server/routes/v2/user-router.ts @@ -2,13 +2,17 @@ import { z } from "zod"; import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { ApiKeysSchema } from "@app/db/schemas/api-keys"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMethod, AuthMode } from "@app/services/auth/auth-type"; export const registerUserRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/me/mfa", method: "PATCH", + url: "/me/mfa", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ isMfaEnabled: z.boolean() @@ -27,8 +31,11 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/me/name", method: "PATCH", + url: "/me/name", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ firstName: z.string().trim(), @@ -48,8 +55,11 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/me/auth-methods", method: "PUT", + url: "/me/auth-methods", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ authMethods: z.nativeEnum(AuthMethod).array().min(1) @@ -70,6 +80,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/me/organizations", + config: { + rateLimit: readLimit + }, schema: { description: "Return organizations that current user is part of", security: [ @@ -93,6 +106,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/me/api-keys", + config: { + rateLimit: readLimit + }, schema: { response: { 200: ApiKeysSchema.omit({ secretHash: true }).array() @@ -108,6 +124,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/me/api-keys", + config: { + rateLimit: writeLimit + }, schema: { body: z.object({ name: z.string().trim(), @@ -130,6 +149,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", url: "/me/api-keys/:apiKeyDataId", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ apiKeyDataId: z.string().trim() @@ -150,6 +172,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/me/sessions", + config: { + rateLimit: readLimit + }, schema: { response: { 200: AuthTokenSessionsSchema.array() @@ -165,6 +190,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", url: "/me/sessions", + config: { + rateLimit: writeLimit + }, schema: { response: { 200: z.object({ @@ -184,6 +212,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/me", + config: { + rateLimit: readLimit + }, schema: { description: "Retrieve the current user on the request", security: [ @@ -207,6 +238,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", url: "/me", + config: { + rateLimit: writeLimit + }, schema: { response: { 200: z.object({ diff --git a/backend/src/server/routes/v3/secret-blind-index-router.ts b/backend/src/server/routes/v3/secret-blind-index-router.ts index 664eaa2c9..cfb27a58e 100644 --- a/backend/src/server/routes/v3/secret-blind-index-router.ts +++ b/backend/src/server/routes/v3/secret-blind-index-router.ts @@ -1,13 +1,17 @@ import { z } from "zod"; import { SecretsSchema } from "@app/db/schemas"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSecretBlindIndexRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/:projectId/secrets/blind-index-status", method: "GET", + url: "/:projectId/secrets/blind-index-status", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ projectId: z.string().trim() @@ -30,8 +34,11 @@ export const registerSecretBlindIndexRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:projectId/secrets", method: "GET", + url: "/:projectId/secrets", + config: { + rateLimit: readLimit + }, schema: { params: z.object({ projectId: z.string().trim() @@ -63,8 +70,11 @@ export const registerSecretBlindIndexRouter = async (server: FastifyZodProvider) }); server.route({ - url: "/:projectId/secrets/names", method: "POST", + url: "/:projectId/secrets/names", + config: { + rateLimit: writeLimit + }, schema: { params: z.object({ projectId: z.string().trim() diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index f69466328..8f4b2b728 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -13,6 +13,7 @@ import { CommitType } from "@app/ee/services/secret-approval-request/secret-appr import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; import { BadRequestError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; +import { secretsLimit, writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getUserAgentType } from "@app/server/plugins/audit-log"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -24,8 +25,11 @@ import { secretRawSchema } from "../sanitizedSchemas"; export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ - url: "/tags/:secretName", method: "POST", + url: "/tags/:secretName", + config: { + rateLimit: writeLimit + }, schema: { description: "Attach tags to a secret", security: [ @@ -83,8 +87,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/tags/:secretName", method: "DELETE", + url: "/tags/:secretName", + config: { + rateLimit: writeLimit + }, schema: { description: "Detach tags from a secret", security: [ @@ -142,8 +149,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/raw", method: "GET", + url: "/raw", + config: { + rateLimit: secretsLimit + }, schema: { description: "List secrets", security: [ @@ -157,6 +167,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), + recursive: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + .describe(RAW_SECRETS.LIST.recursive), include_imports: z .enum(["true", "false"]) .default("false") @@ -165,7 +180,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - secrets: secretRawSchema.array(), + secrets: secretRawSchema + .extend({ + secretPath: z.string().optional() + }) + .array(), imports: z .object({ secretPath: z.string(), @@ -218,7 +237,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, projectId: workspaceId, path: secretPath, - includeImports: req.query.include_imports + includeImports: req.query.include_imports, + recursive: req.query.recursive }); await server.services.auditLog.createAuditLog({ @@ -251,8 +271,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/raw/:secretName", method: "GET", + url: "/raw/:secretName", + config: { + rateLimit: secretsLimit + }, schema: { description: "Get a secret by name", security: [ @@ -343,8 +366,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/raw/:secretName", method: "POST", + url: "/raw/:secretName", + config: { + rateLimit: secretsLimit + }, schema: { description: "Create secret", security: [ @@ -429,8 +455,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/raw/:secretName", method: "PATCH", + url: "/raw/:secretName", + config: { + rateLimit: secretsLimit + }, schema: { description: "Update secret", security: [ @@ -512,8 +541,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/raw/:secretName", method: "DELETE", + url: "/raw/:secretName", + config: { + rateLimit: secretsLimit + }, schema: { description: "Delete secret", security: [ @@ -589,13 +621,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/", method: "GET", + url: "/", + config: { + rateLimit: secretsLimit + }, schema: { querystring: z.object({ workspaceId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/").transform(removeTrailingSlash), + recursive: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true"), include_imports: z .enum(["true", "false"]) .default("false") @@ -604,19 +643,18 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ secrets: SecretsSchema.omit({ secretBlindIndex: true }) - .merge( - z.object({ - _id: z.string(), - workspace: z.string(), - environment: z.string(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - name: true, - color: true - }).array() - }) - ) + .extend({ + _id: z.string(), + workspace: z.string(), + environment: z.string(), + secretPath: z.string().optional(), + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + name: true, + color: true + }).array() + }) .array(), imports: z .object({ @@ -648,7 +686,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { environment: req.query.environment, projectId: req.query.workspaceId, path: req.query.secretPath, - includeImports: req.query.include_imports + includeImports: req.query.include_imports, + recursive: req.query.recursive }); await server.services.auditLog.createAuditLog({ @@ -697,8 +736,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:secretName", method: "GET", + url: "/:secretName", + config: { + rateLimit: secretsLimit + }, schema: { params: z.object({ secretName: z.string().trim() @@ -775,6 +817,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ url: "/:secretName", method: "POST", + config: { + rateLimit: secretsLimit + }, schema: { body: z.object({ workspaceId: z.string().trim(), @@ -941,8 +986,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:secretName", method: "PATCH", + url: "/:secretName", + config: { + rateLimit: secretsLimit + }, schema: { params: z.object({ secretName: z.string() @@ -1125,8 +1173,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:secretName", method: "DELETE", + url: "/:secretName", + config: { + rateLimit: secretsLimit + }, schema: { params: z.object({ secretName: z.string() @@ -1246,8 +1297,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/batch", method: "POST", + url: "/batch", + config: { + rateLimit: secretsLimit + }, schema: { body: z.object({ workspaceId: z.string().trim(), @@ -1369,8 +1423,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/batch", method: "PATCH", + url: "/batch", + config: { + rateLimit: secretsLimit + }, schema: { body: z.object({ workspaceId: z.string().trim(), @@ -1492,8 +1549,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/batch", method: "DELETE", + url: "/batch", + config: { + rateLimit: secretsLimit + }, schema: { body: z.object({ workspaceId: z.string().trim(), diff --git a/backend/src/server/routes/v3/user-router.ts b/backend/src/server/routes/v3/user-router.ts index 1672405b1..a9fdba358 100644 --- a/backend/src/server/routes/v3/user-router.ts +++ b/backend/src/server/routes/v3/user-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { ApiKeysSchema } from "@app/db/schemas/api-keys"; +import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -8,6 +9,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/me/api-keys", + config: { + rateLimit: readLimit + }, schema: { response: { 200: z.object({ diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index e1b10530d..fa7439af8 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -153,7 +153,7 @@ export const authLoginServiceFactory = ({ username: email }); if (!userEnc || (userEnc && !userEnc.isAccepted)) { - throw new Error("Failed to find user"); + throw new Error("Failed to find user"); } if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) { validateProviderAuthToken(providerAuthToken as string, email); diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 42b8c2c39..4025e4903 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -192,7 +192,7 @@ export const authPaswordServiceFactory = ({ }: TCreateBackupPrivateKeyDTO) => { const userEnc = await userDAL.findUserEncKeyByUserId(userId); if (!userEnc || (userEnc && !userEnc.isAccepted)) { - throw new Error("Failed to find user"); + throw new Error("Failed to find user"); } if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey) throw new Error("failed to create backup key"); @@ -239,7 +239,7 @@ export const authPaswordServiceFactory = ({ const getBackupPrivateKeyOfUser = async (userId: string) => { const user = await userDAL.findUserEncKeyByUserId(userId); if (!user || (user && !user.isAccepted)) { - throw new Error("Failed to find user"); + throw new Error("Failed to find user"); } const backupKey = await authDAL.getBackupPrivateKeyByUserId(userId); if (!backupKey) throw new Error("Failed to find user backup key"); diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts index a3c53658c..8e7b92253 100644 --- a/backend/src/services/auth/auth-type.ts +++ b/backend/src/services/auth/auth-type.ts @@ -7,6 +7,7 @@ export enum AuthMethod { AZURE_SAML = "azure-saml", JUMPCLOUD_SAML = "jumpcloud-saml", GOOGLE_SAML = "google-saml", + KEYCLOAK_SAML = "keycloak-saml", LDAP = "ldap" } diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 5c8ecdcff..831af3200 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -14,16 +14,25 @@ import { import { BadRequestError } from "@app/lib/errors"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; +import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; +import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectRoleDALFactory } from "./project-role-dal"; type TProjectRoleServiceFactoryDep = { projectRoleDAL: TProjectRoleDALFactory; permissionService: Pick; + identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory; + projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory; }; export type TProjectRoleServiceFactory = ReturnType; -export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: TProjectRoleServiceFactoryDep) => { +export const projectRoleServiceFactory = ({ + projectRoleDAL, + permissionService, + identityProjectMembershipRoleDAL, + projectUserMembershipRoleDAL +}: TProjectRoleServiceFactoryDep) => { const createRole = async ( actor: ActorType, actorId: string, @@ -96,8 +105,25 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role); + + const identityRole = await identityProjectMembershipRoleDAL.findOne({ customRoleId: roleId }); + const projectUserRole = await projectUserMembershipRoleDAL.findOne({ customRoleId: roleId }); + + if (identityRole) { + throw new BadRequestError({ + message: "The role is assigned to one or more identities. Make sure to unassign them before deleting the role.", + name: "Delete role" + }); + } + if (projectUserRole) { + throw new BadRequestError({ + message: "The role is assigned to one or more users. Make sure to unassign them before deleting the role.", + name: "Delete role" + }); + } + const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId }); - if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" }); + if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Delete role" }); return deletedRole; }; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 2970d31bd..9e8d70020 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -6,6 +6,7 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { createSecretBlindIndex } from "@app/lib/crypto"; @@ -65,6 +66,7 @@ type TProjectServiceFactoryDep = { orgService: Pick; licenseService: Pick; orgDAL: Pick; + keyStore: Pick; }; export type TProjectServiceFactory = ReturnType; @@ -86,7 +88,8 @@ export const projectServiceFactory = ({ projectEnvDAL, licenseService, projectUserMembershipRoleDAL, - identityProjectMembershipRoleDAL + identityProjectMembershipRoleDAL, + keyStore }: TProjectServiceFactoryDep) => { /* * Create workspace. Make user the admin @@ -323,6 +326,7 @@ export const projectServiceFactory = ({ }; }); + await keyStore.deleteItem(`infisical-cloud-plan-${actorOrgId}`); return results; }; @@ -350,6 +354,7 @@ export const projectServiceFactory = ({ return delProject; }); + await keyStore.deleteItem(`infisical-cloud-plan-${actorOrgId}`); return deletedProject; }; diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts index a2ad4d82c..aa45d410d 100644 --- a/backend/src/services/secret-import/secret-import-dal.ts +++ b/backend/src/services/secret-import/secret-import-dal.ts @@ -70,9 +70,31 @@ export const secretImportDALFactory = (db: TDbClient) => { } }; + const findByFolderIds = async (folderIds: string[], tx?: Knex) => { + try { + const docs = await (tx || db)(TableName.SecretImport) + .whereIn("folderId", folderIds) + .join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`) + .select( + db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports, + db.ref("slug").withSchema(TableName.Environment), + db.ref("name").withSchema(TableName.Environment), + db.ref("id").withSchema(TableName.Environment).as("envId") + ) + .orderBy("position", "asc"); + return docs.map(({ envId, slug, name, ...el }) => ({ + ...el, + importEnv: { id: envId, slug, name } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find secret imports" }); + } + }; + return { ...secretImportOrm, find, + findByFolderIds, findLastImportPosition, updateAllPosition }; diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index 504174765..8a5970b83 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -171,6 +171,50 @@ export const secretDALFactory = (db: TDbClient) => { } }; + const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => { + try { + // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) + if (userId && !uuidValidate(userId)) { + // eslint-disable-next-line no-param-reassign + userId = undefined; + } + + const secs = await (tx || db)(TableName.Secret) + .whereIn("folderId", folderIds) + .where((bd) => { + void bd.whereNull("userId").orWhere({ userId: userId || null }); + }) + .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) + .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) + .select(selectAllTableCols(TableName.Secret)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) + .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName")) + .orderBy("id", "asc"); + const data = sqlNestRelationships({ + data: secs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ + id, + color, + slug, + name + }) + } + ] + }); + return data; + } catch (error) { + throw new DatabaseError({ error, name: "get all secret" }); + } + }; + const findByBlindIndexes = async ( folderId: string, blindIndexes: Array<{ blindIndex: string; type: SecretType }>, @@ -207,6 +251,7 @@ export const secretDALFactory = (db: TDbClient) => { bulkUpdateNoVersionIncrement, getSecretTags, findByFolderId, + findByFolderIds, findByBlindIndexes }; }; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 212bb01f0..24c2bd811 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -1,4 +1,5 @@ /* eslint-disable no-await-in-loop */ +import { subject } from "@casl/ability"; import path from "path"; import { @@ -7,8 +8,11 @@ import { SecretType, TableName, TSecretBlindIndexes, + TSecretFolders, TSecrets } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { buildSecretBlindIndexFromName, @@ -18,7 +22,9 @@ import { import { BadRequestError } from "@app/lib/errors"; import { groupBy, unique } from "@app/lib/fn"; +import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { getBotKeyFnFactory } from "../project-bot/project-bot-fns"; +import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretDALFactory } from "./secret-dal"; import { @@ -45,6 +51,133 @@ export const generateSecretBlindIndexBySalt = async (secretName: string, secretB return secretBlindIndex; }; +type TRecursivelyFetchSecretsFromFoldersArg = { + permissionService: Pick; + folderDAL: Pick; + projectEnvDAL: Pick; +}; + +type TGetPathsDTO = { + projectId: string; + environment: string; + currentPath: string; + + auth: { + actor: ActorType; + actorId: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId: string | undefined; + }; +}; + +// Introduce a new interface for mapping parent IDs to their children +interface FolderMap { + [parentId: string]: TSecretFolders[]; +} +const buildHierarchy = (folders: TSecretFolders[]): FolderMap => { + const map: FolderMap = {}; + map.null = []; // Initialize mapping for root directory + + folders.forEach((folder) => { + const parentId = folder.parentId || "null"; + if (!map[parentId]) { + map[parentId] = []; + } + map[parentId].push(folder); + }); + + return map; +}; + +const generatePaths = ( + map: FolderMap, + parentId: string = "null", + basePath: string = "" +): { path: string; folderId: string }[] => { + const children = map[parentId || "null"] || []; + let paths: { path: string; folderId: string }[] = []; + + children.forEach((child) => { + // Determine if this is the root folder of the environment. If no parentId is present and the name is root, it's the root folder + const isRootFolder = child.name === "root" && !child.parentId; + + // Form the current path based on the base path and the current child + // eslint-disable-next-line no-nested-ternary + const currPath = basePath === "" ? (isRootFolder ? "/" : `/${child.name}`) : `${basePath}/${child.name}`; + + paths.push({ + path: currPath, + folderId: child.id + }); // Add the current path + + // Recursively generate paths for children, passing down the formatted pathh + const childPaths = generatePaths(map, child.id, currPath); + paths = paths.concat( + childPaths.map((p) => ({ + path: p.path, + folderId: p.folderId + })) + ); + }); + + return paths; +}; + +export const recursivelyGetSecretPaths = ({ + folderDAL, + projectEnvDAL, + permissionService +}: TRecursivelyFetchSecretsFromFoldersArg) => { + const getPaths = async ({ projectId, environment, currentPath, auth }: TGetPathsDTO) => { + const env = await projectEnvDAL.findOne({ + projectId, + slug: environment + }); + + if (!env) { + throw new Error(`'${environment}' environment not found in project with ID ${projectId}`); + } + + // Fetch all folders in env once with a single query + const folders = await folderDAL.find({ + envId: env.id + }); + + // Build the folder hierarchy map + const folderMap = buildHierarchy(folders); + + // Generate the paths paths and normalize the root path to / + const paths = generatePaths(folderMap).map((p) => ({ + path: p.path === "/" ? p.path : p.path.substring(1), + folderId: p.folderId + })); + + const { permission } = await permissionService.getProjectPermission( + auth.actor, + auth.actorId, + projectId, + auth.actorAuthMethod, + auth.actorOrgId + ); + + // Filter out paths that the user does not have permission to access, and paths that are not in the current path + const allowedPaths = paths.filter( + (folder) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: folder.path + }) + ) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) + ); + + return allowedPaths; + }; + + return getPaths; +}; + type TInterpolateSecretArg = { projectId: string; secretEncKey: string; @@ -202,9 +335,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD ); // eslint-disable-next-line - secrets[key].value = secrets[key].skipMultilineEncoding - ? expandedVal - : formatMultiValueEnv(expandedVal); + secrets[key].value = secrets[key].skipMultilineEncoding ? expandedVal : formatMultiValueEnv(expandedVal); } return secrets; @@ -212,7 +343,10 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD return expandSecrets; }; -export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environment: string }, key: string) => { +export const decryptSecretRaw = ( + secret: TSecrets & { workspace: string; environment: string; secretPath?: string }, + key: string +) => { const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretKeyCiphertext, iv: secret.secretKeyIV, @@ -240,6 +374,7 @@ export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environ return { secretKey, + secretPath: secret.secretPath, workspace: secret.workspace, environment: secret.environment, secretValue, diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index ed96edb41..3b504fbf4 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-unreachable-loop */ +/* eslint-disable no-await-in-loop */ import { ForbiddenError, subject } from "@casl/ability"; import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType } from "@app/db/schemas"; @@ -13,13 +15,20 @@ import { logger } from "@app/lib/logger"; import { ActorType } from "../auth/auth-type"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; +import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsFromImports } from "../secret-import/secret-import-fns"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; import { TSecretDALFactory } from "./secret-dal"; -import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns"; +import { + decryptSecretRaw, + fnSecretBlindIndexCheck, + fnSecretBulkInsert, + fnSecretBulkUpdate, + recursivelyGetSecretPaths +} from "./secret-fns"; import { TSecretQueueFactory } from "./secret-queue"; import { TAttachSecretTagsDTO, @@ -47,20 +56,25 @@ type TSecretServiceFactoryDep = { secretDAL: TSecretDALFactory; secretTagDAL: TSecretTagDALFactory; secretVersionDAL: TSecretVersionDALFactory; - folderDAL: Pick; projectDAL: Pick; + projectEnvDAL: Pick; + folderDAL: Pick< + TSecretFolderDALFactory, + "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" + >; secretBlindIndexDAL: TSecretBlindIndexDALFactory; permissionService: Pick; snapshotService: Pick; secretQueueService: Pick; projectBotService: Pick; - secretImportDAL: Pick; + secretImportDAL: Pick; secretVersionTagDAL: Pick; }; export type TSecretServiceFactory = ReturnType; export const secretServiceFactory = ({ secretDAL, + projectEnvDAL, secretTagDAL, secretVersionDAL, folderDAL, @@ -425,7 +439,8 @@ export const secretServiceFactory = ({ actor, actorOrgId, actorAuthMethod, - includeImports + includeImports, + recursive }: TGetSecretsDTO) => { const { permission } = await permissionService.getProjectPermission( actor, @@ -434,19 +449,52 @@ export const secretServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + + let paths: { folderId: string; path: string }[] = []; + + if (recursive) { + const getPaths = recursivelyGetSecretPaths({ + permissionService, + folderDAL, + projectEnvDAL + }); + + const deepPaths = await getPaths({ + projectId, + environment, + currentPath: path, + auth: { + actor, + actorId, + actorAuthMethod, + actorOrgId + } + }); + + if (!deepPaths) return { secrets: [], imports: [] }; + + paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + ); + + const folder = await folderDAL.findBySecretPath(projectId, environment, path); + if (!folder) return { secrets: [], imports: [] }; + + paths = [{ folderId: folder.id, path }]; + } + + const groupedPaths = groupBy(paths, (p) => p.folderId); + + const secrets = await secretDAL.findByFolderIds( + paths.map((p) => p.folderId), + actorId ); - const folder = await folderDAL.findBySecretPath(projectId, environment, path); - if (!folder) return { secrets: [], imports: [] }; - const folderId = folder.id; - - const secrets = await secretDAL.findByFolderId(folderId, actorId); - if (includeImports) { - const secretImports = await secretImportDAL.find({ folderId }); + const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId)); const allowedImports = secretImports.filter(({ importEnv, importPath }) => // if its service token allow full access over imported one actor === ActorType.SERVICE @@ -464,12 +512,26 @@ export const secretServiceFactory = ({ secretDAL, folderDAL }); + return { - secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })), + secrets: secrets.map((secret) => ({ + ...secret, + workspace: projectId, + environment, + secretPath: groupedPaths[secret.folderId][0].path + })), imports: importedSecrets }; } - return { secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })) }; + + return { + secrets: secrets.map((secret) => ({ + ...secret, + workspace: projectId, + environment, + secretPath: groupedPaths[secret.folderId][0].path + })) + }; }; const getSecretByName = async ({ @@ -789,7 +851,8 @@ export const secretServiceFactory = ({ actorOrgId, actorAuthMethod, environment, - includeImports + includeImports, + recursive }: TGetSecretsRawDTO) => { const botKey = await projectBotService.getBotKey(projectId); if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); @@ -802,7 +865,8 @@ export const secretServiceFactory = ({ actorOrgId, actorAuthMethod, path, - includeImports + includeImports, + recursive }); return { @@ -810,7 +874,10 @@ export const secretServiceFactory = ({ imports: (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({ ...el, secrets: importedSecrets.map((sec) => - decryptSecretRaw({ ...sec, environment: el.environment, workspace: projectId }, botKey) + decryptSecretRaw( + { ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath }, + botKey + ) ) })) }; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index efd4f0f8b..22347de4e 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -74,6 +74,7 @@ export type TGetSecretsDTO = { path: string; environment: string; includeImports?: boolean; + recursive?: boolean; } & TProjectPermission; export type TGetASecretDTO = { @@ -140,6 +141,7 @@ export type TGetSecretsRawDTO = { path: string; environment: string; includeImports?: boolean; + recursive?: boolean; } & TProjectPermission; export type TGetASecretRawDTO = { diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 38d82a0a5..d45a42db4 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -277,6 +277,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req SetQueryParam("environment", request.Environment). SetQueryParam("workspaceId", request.WorkspaceId) + if request.Recursive { + httpRequest.SetQueryParam("recursive", "true") + } + if request.IncludeImport { httpRequest.SetQueryParam("include_imports", "true") } diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index b49cb1581..ed9a1d629 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -291,6 +291,7 @@ type GetEncryptedSecretsV3Request struct { WorkspaceId string `json:"workspaceId"` SecretPath string `json:"secretPath"` IncludeImport bool `json:"include_imports"` + Recursive bool `json:"recursive"` } type GetFoldersV1Request struct { @@ -510,7 +511,7 @@ type CreateDynamicSecretLeaseV1Request struct { type CreateDynamicSecretLeaseV1Response struct { Lease struct { - Id string `json:"id"` + Id string `json:"id"` ExpireAt time.Time `json:"expireAt"` } `json:"lease"` DynamicSecret struct { diff --git a/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go index ce3f7a8b0..80f321396 100644 --- a/cli/packages/cmd/agent.go +++ b/cli/packages/cmd/agent.go @@ -332,7 +332,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) { func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) { return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) { - res, err := util.GetPlainTextSecretsViaMachineIdentity(accessToken, projectID, envSlug, secretPath, false) + res, err := util.GetPlainTextSecretsViaMachineIdentity(accessToken, projectID, envSlug, secretPath, false, false) if err != nil { return nil, err } diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index d008af1ec..cb44e3d7e 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -98,7 +98,12 @@ var runCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports}, projectConfigDir) + recursive, err := cmd.Flags().GetBool("recursive") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports, Recursive: recursive}, projectConfigDir) if err != nil { util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") @@ -202,6 +207,7 @@ func init() { runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") runCmd.Flags().Bool("include-imports", true, "Import linked secrets ") + runCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index cf9c89b47..4e1b09c9e 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -63,6 +63,11 @@ var secretsCmd = &cobra.Command{ util.HandleError(err) } + recursive, err := cmd.Flags().GetBool("recursive") + if err != nil { + util.HandleError(err) + } + tagSlugs, err := cmd.Flags().GetString("tags") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -73,7 +78,7 @@ var secretsCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports}, "") + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports, Recursive: recursive}, "") if err != nil { util.HandleError(err) } @@ -413,12 +418,17 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse path flag") } + recursive, err := cmd.Flags().GetBool("recursive") + if err != nil { + util.HandleError(err, "Unable to parse recursive flag") + } + showOnlyValue, err := cmd.Flags().GetBool("raw-value") if err != nil { util.HandleError(err, "Unable to parse path flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true}, "") + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true, Recursive: recursive}, "") if err != nil { util.HandleError(err, "To fetch all secrets") } @@ -683,6 +693,7 @@ func init() { secretsCmd.AddCommand(secretsGetCmd) secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path") secretsGetCmd.Flags().Bool("raw-value", false, "Returns only the value of secret, only works with one secret") + secretsGetCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") secretsCmd.AddCommand(secretsSetCmd) @@ -727,6 +738,7 @@ func init() { secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ") + secretsCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs") secretsCmd.Flags().String("path", "/", "get secrets within a folder path") rootCmd.AddCommand(secretsCmd) diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 4a7dc782a..e197218e0 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -93,6 +93,7 @@ type GetAllSecretsParameters struct { WorkspaceId string SecretsPath string IncludeImport bool + Recursive bool } type GetAllFoldersParameters struct { diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 6f56bc84a..23e95db64 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -17,7 +17,7 @@ import ( "github.com/rs/zerolog/log" ) -func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) { +func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) { serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) if len(serviceTokenParts) < 4 { return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") @@ -49,6 +49,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str Environment: environment, SecretPath: secretPath, IncludeImport: includeImports, + Recursive: recursive, }) if err != nil { @@ -80,7 +81,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str return plainTextSecrets, serviceTokenDetails, nil } -func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool) ([]models.SingleEnvironmentVariable, error) { +func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, error) { httpClient := resty.New() httpClient.SetAuthToken(JTWToken). SetHeader("Accept", "application/json") @@ -125,6 +126,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work WorkspaceId: workspaceId, Environment: environmentName, IncludeImport: includeImports, + Recursive: recursive, // TagSlugs: tagSlugs, } @@ -152,7 +154,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work return plainTextSecrets, nil } -func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool) (models.PlaintextSecretResult, error) { +func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool) (models.PlaintextSecretResult, error) { httpClient := resty.New() httpClient.SetAuthToken(accessToken). SetHeader("Accept", "application/json") @@ -161,6 +163,7 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin WorkspaceId: workspaceId, Environment: environmentName, IncludeImport: includeImports, + Recursive: recursive, // TagSlugs: tagSlugs, } @@ -329,7 +332,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo } secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, infisicalDotJson.WorkspaceId, - params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport) + params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport, params.Recursive) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32] @@ -350,10 +353,10 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo } else { if params.InfisicalToken != "" { log.Debug().Msg("Trying to fetch secrets using service token") - secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport) + secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive) } else if params.UniversalAuthAccessToken != "" { log.Debug().Msg("Trying to fetch secrets using universal auth") - res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport) + res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive) errorToReturn = err secretsToReturn = res.Secrets diff --git a/docs/api-reference/endpoints/identity-specific-privilege/create-permanent.mdx b/docs/api-reference/endpoints/identity-specific-privilege/create-permanent.mdx new file mode 100644 index 000000000..8e02c28a3 --- /dev/null +++ b/docs/api-reference/endpoints/identity-specific-privilege/create-permanent.mdx @@ -0,0 +1,4 @@ +--- +title: "Create Permanent" +openapi: "POST /api/v1/additional-privilege/identity/permanent" +--- diff --git a/docs/api-reference/endpoints/identity-specific-privilege/create-temporary.mdx b/docs/api-reference/endpoints/identity-specific-privilege/create-temporary.mdx new file mode 100644 index 000000000..808f27859 --- /dev/null +++ b/docs/api-reference/endpoints/identity-specific-privilege/create-temporary.mdx @@ -0,0 +1,4 @@ +--- +title: "Create Temporary" +openapi: "POST /api/v1/additional-privilege/identity/temporary" +--- diff --git a/docs/api-reference/endpoints/identity-specific-privilege/delete.mdx b/docs/api-reference/endpoints/identity-specific-privilege/delete.mdx new file mode 100644 index 000000000..430282789 --- /dev/null +++ b/docs/api-reference/endpoints/identity-specific-privilege/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/additional-privilege/identity" +--- diff --git a/docs/api-reference/endpoints/identity-specific-privilege/find-by-slug.mdx b/docs/api-reference/endpoints/identity-specific-privilege/find-by-slug.mdx new file mode 100644 index 000000000..a6ec27217 --- /dev/null +++ b/docs/api-reference/endpoints/identity-specific-privilege/find-by-slug.mdx @@ -0,0 +1,4 @@ +--- +title: "Find By Privilege Slug" +openapi: "GET /api/v1/additional-privilege/identity/{privilegeSlug}" +--- diff --git a/docs/api-reference/endpoints/identity-specific-privilege/list.mdx b/docs/api-reference/endpoints/identity-specific-privilege/list.mdx new file mode 100644 index 000000000..4698ed838 --- /dev/null +++ b/docs/api-reference/endpoints/identity-specific-privilege/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/additional-privilege/identity" +--- diff --git a/docs/api-reference/endpoints/identity-specific-privilege/update.mdx b/docs/api-reference/endpoints/identity-specific-privilege/update.mdx new file mode 100644 index 000000000..987d6ac8c --- /dev/null +++ b/docs/api-reference/endpoints/identity-specific-privilege/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/additional-privilege/identity" +--- diff --git a/docs/api-reference/endpoints/integrations/create-auth.mdx b/docs/api-reference/endpoints/integrations/create-auth.mdx new file mode 100644 index 000000000..5af7a0f9c --- /dev/null +++ b/docs/api-reference/endpoints/integrations/create-auth.mdx @@ -0,0 +1,32 @@ +--- +title: "Create Auth" +openapi: "POST /api/v1/integration-auth/access-token" +--- + +## Integration Authentication Parameters + +The integration authentication endpoint is generic and can be used for all native integrations. +For specific integration parameters for a given service, please review the respective documentation below. + + + + + This value must be **aws-secret-manager**. + + + Infisical project id for the integration. + + + The AWS IAM User Access ID. + + + The AWS IAM User Access Secret Key. + + + + Coming Soon + + + Coming Soon + + diff --git a/docs/api-reference/endpoints/integrations/create.mdx b/docs/api-reference/endpoints/integrations/create.mdx new file mode 100644 index 000000000..0992e91b9 --- /dev/null +++ b/docs/api-reference/endpoints/integrations/create.mdx @@ -0,0 +1,40 @@ +--- +title: "Create" +openapi: "POST /api/v1/integration" +--- + +## Integration Parameters + +The integration creation endpoint is generic and can be used for all native integrations. +For specific integration parameters for a given service, please review the respective documentation below. + + + + + The ID of the integration auth object for authentication with AWS. + Refer [Create Integration Auth](./create-auth) for more info + + + Whether the integration should be active or inactive + + + The secret name used when saving secret in AWS SSM. Used for naming and can be arbitrary. + + + The AWS region of the SSM. Example: `us-east-1` + + + The Infisical environment slug from where secrets will be synced from. Example: `dev` + + + The Infisical folder path from where secrets will be synced from. Example: `/some/path`. The root of the environment is `/`. + + + + Coming Soon + + + Coming Soon + + + diff --git a/docs/api-reference/endpoints/integrations/delete-auth-by-id.mdx b/docs/api-reference/endpoints/integrations/delete-auth-by-id.mdx new file mode 100644 index 000000000..5884363fc --- /dev/null +++ b/docs/api-reference/endpoints/integrations/delete-auth-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete Auth By ID" +openapi: "DELETE /api/v1/integration-auth/{integrationAuthId}" +--- diff --git a/docs/api-reference/endpoints/integrations/delete-auth.mdx b/docs/api-reference/endpoints/integrations/delete-auth.mdx new file mode 100644 index 000000000..93d957903 --- /dev/null +++ b/docs/api-reference/endpoints/integrations/delete-auth.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete Auth" +openapi: "DELETE /api/v1/integration-auth" +--- diff --git a/docs/api-reference/endpoints/integrations/delete.mdx b/docs/api-reference/endpoints/integrations/delete.mdx new file mode 100644 index 000000000..51df56de7 --- /dev/null +++ b/docs/api-reference/endpoints/integrations/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/integration/{integrationId}" +--- diff --git a/docs/api-reference/endpoints/integrations/find-auth.mdx b/docs/api-reference/endpoints/integrations/find-auth.mdx new file mode 100644 index 000000000..439b82935 --- /dev/null +++ b/docs/api-reference/endpoints/integrations/find-auth.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Auth By ID" +openapi: "GET /api/v1/integration-auth/{integrationAuthId}" +--- diff --git a/docs/api-reference/endpoints/integrations/list-auth.mdx b/docs/api-reference/endpoints/integrations/list-auth.mdx new file mode 100644 index 000000000..3ca961d98 --- /dev/null +++ b/docs/api-reference/endpoints/integrations/list-auth.mdx @@ -0,0 +1,4 @@ +--- +title: "List Auth" +openapi: "GET /api/v1/workspace/{workspaceId}/authorizations" +--- diff --git a/docs/api-reference/endpoints/integrations/update.mdx b/docs/api-reference/endpoints/integrations/update.mdx new file mode 100644 index 000000000..8567c46ae --- /dev/null +++ b/docs/api-reference/endpoints/integrations/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/integration/{integrationId}" +--- diff --git a/docs/api-reference/overview/examples/e2ee-disabled.mdx b/docs/api-reference/overview/examples/e2ee-disabled.mdx deleted file mode 100644 index 1a9e57552..000000000 --- a/docs/api-reference/overview/examples/e2ee-disabled.mdx +++ /dev/null @@ -1,180 +0,0 @@ ---- -title: "E2EE Disabled" ---- - -Using Infisical's API to read/write secrets with E2EE disabled allows you to create, update, and retrieve secrets -in plaintext. Effectively, this means each such secret operation only requires 1 HTTP call. - - - - Retrieve all secrets for an Infisical project and environment. - - - ```bash - curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw?environment=environment&workspaceId=workspaceId' \ - --header 'Authorization: Bearer serviceToken' - - ``` - - - #### - - When using a [service token](../../../documentation/platform/token) with access to a single environment and path, you don't need to provide request parameters because the server will automatically scope the request to the defined environment/secrets path of the service token used. - For all other cases, request parameters are required. - - #### - - The ID of the workspace - - - The environment slug - - - Path to secrets in workspace - - - - Create a secret in Infisical. - - - - ```bash - curl --location --request POST 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ - --header 'Authorization: Bearer serviceToken' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "workspaceId": "workspaceId", - "environment": "environment", - "type": "shared", - "secretValue": "secretValue", - "secretPath": "/" - }' - ``` - - - - - Name of secret to create - - - The ID of the workspace - - - The environment slug - - - Value of secret - - - Comment of secret - - - Path to secret in workspace - - - The type of the secret. Valid options are “shared” or “personal” - - - - Retrieve a secret from Infisical. - - - - ```bash - curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw/secretName?workspaceId=workspaceId&environment=environment' \ - --header 'Authorization: Bearer serviceToken' - ``` - - - - - Name of secret to retrieve - - - The ID of the workspace - - - The environment slug - - - Path to secrets in workspace - - - The type of the secret. Valid options are “shared” or “personal” - - - - Update an existing secret in Infisical. - - - - ```bash - curl --location --request PATCH 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ - --header 'Authorization: Bearer serviceToken' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "workspaceId": "workspaceId", - "environment": "environment", - "type": "shared", - "secretValue": "secretValue", - "secretPath": "/" - }' - ``` - - - - - Name of secret to update - - - The ID of the workspace - - - The environment slug - - - Value of secret - - - Path to secret in workspace. - - - The type of the secret. Valid options are “shared” or “personal” - - - - Delete a secret in Infisical. - - - - ```bash - curl --location --request DELETE 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ - --header 'Authorization: Bearer serviceToken' \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "workspaceId": "workspaceId", - "environment": "environment", - "type": "shared", - "secretPath": "/" - }' - ``` - - - - - Name of secret to update - - - The ID of the workspace - - - The environment slug - - - Path to secret in workspace. - - - The type of the secret. Valid options are “shared” or “personal” - - - \ No newline at end of file diff --git a/docs/api-reference/overview/examples/e2ee-enabled.mdx b/docs/api-reference/overview/examples/e2ee-enabled.mdx deleted file mode 100644 index 1de9c2290..000000000 --- a/docs/api-reference/overview/examples/e2ee-enabled.mdx +++ /dev/null @@ -1,862 +0,0 @@ ---- -title: "E2EE Enabled" ---- - - - E2EE enabled mode only works with [Service Tokens](/documentation/platform/token) and cannot be used with [Identities](/documentation/platform/identities/overview). - - -Using Infisical's API to read/write secrets with E2EE enabled allows you to create, update, and retrieve secrets -but requires you to perform client-side encryption/decryption operations. For this reason, we recommend using one of the available -SDKs instead. - - - - - - Retrieve all secrets for an Infisical project and environment. -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const getSecrets = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Get secrets for your project and environment - const { data } = await axios.get( - `${BASE_URL}/api/v3/secrets?${new URLSearchParams({ - environment: serviceTokenData.environment, - workspaceId: serviceTokenData.workspace - })}`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - const encryptedSecrets = data.secrets; - - // 3. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 4. Decrypt the (encrypted) secrets - const secrets = encryptedSecrets.map((secret) => { - const secretKey = decrypt({ - ciphertext: secret.secretKeyCiphertext, - iv: secret.secretKeyIV, - tag: secret.secretKeyTag, - secret: projectKey - }); - - const secretValue = decrypt({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - secret: projectKey - }); - - return ({ - secretKey, - secretValue - }); - }); - - console.log('secrets: ', secrets); -} - -getSecrets(); - -``` - - - -```Python -import requests -import base64 -from Cryptodome.Cipher import AES - - -BASE_URL = "http://app.infisical.com" - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def get_secrets(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Get secrets for your project and environment - data = requests.get( - f"{BASE_URL}/api/v3/secrets", - params={ - "environment": service_token_data["environment"], - "workspaceId": service_token_data["workspace"], - }, - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - encrypted_secrets = data["secrets"] - - # 3. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 4. Decrypt the (encrypted) secrets - secrets = [] - for secret in encrypted_secrets: - secret_key = decrypt( - ciphertext=secret["secretKeyCiphertext"], - iv=secret["secretKeyIV"], - tag=secret["secretKeyTag"], - secret=project_key, - ) - - secret_value = decrypt( - ciphertext=secret["secretValueCiphertext"], - iv=secret["secretValueIV"], - tag=secret["secretValueTag"], - secret=project_key, - ) - - secrets.append( - { - "secret_key": secret_key, - "secret_value": secret_value, - } - ) - - print("secrets:", secrets) - - -get_secrets() - -``` - - - - - - -Create a secret in Infisical. -```js -const crypto = require('crypto'); -const axios = require('axios'); -const nacl = require('tweetnacl'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; -const BLOCK_SIZE_BYTES = 16; - -const encrypt = ({ text, secret }) => { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); - return { - ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') - }; -} - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const createSecrets = async () => { - const serviceToken = ''; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared'; // 'shared' or 'personal' - const secretKey = 'some_key'; - const secretValue = 'some_value'; - const secretComment = 'some_comment'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 3. Encrypt your secret with the project key - const { - ciphertext: secretKeyCiphertext, - iv: secretKeyIV, - tag: secretKeyTag - } = encrypt({ - text: secretKey, - secret: projectKey - }); - - const { - ciphertext: secretValueCiphertext, - iv: secretValueIV, - tag: secretValueTag - } = encrypt({ - text: secretValue, - secret: projectKey - }); - - const { - ciphertext: secretCommentCiphertext, - iv: secretCommentIV, - tag: secretCommentTag - } = encrypt({ - text: secretComment, - secret: projectKey - }); - - // 4. Send (encrypted) secret to Infisical - await axios.post( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); -} - -createSecrets(); -``` - - - -```Python -import base64 -import requests -from Cryptodome.Cipher import AES -from Cryptodome.Random import get_random_bytes - - -BASE_URL = "https://app.infisical.com" -BLOCK_SIZE_BYTES = 16 - - -def encrypt(text, secret): - iv = get_random_bytes(BLOCK_SIZE_BYTES) - secret = bytes(secret, "utf-8") - cipher = AES.new(secret, AES.MODE_GCM, iv) - ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8")) - return { - "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"), - "tag": base64.standard_b64encode(tag).decode("utf-8"), - "iv": base64.standard_b64encode(iv).decode("utf-8"), - } - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def create_secrets(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared or "personal" - secret_key = "some_key" - secret_value = "some_value" - secret_comment = "some_comment" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 3. Encrypt your secret with the project key - encrypted_key_data = encrypt(text=secret_key, secret=project_key) - encrypted_value_data = encrypt(text=secret_value, secret=project_key) - encrypted_comment_data = encrypt(text=secret_comment, secret=project_key) - - # 4. Send (encrypted) secret to Infisical - requests.post( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type, - "secretKeyCiphertext": encrypted_key_data["ciphertext"], - "secretKeyIV": encrypted_key_data["iv"], - "secretKeyTag": encrypted_key_data["tag"], - "secretValueCiphertext": encrypted_value_data["ciphertext"], - "secretValueIV": encrypted_value_data["iv"], - "secretValueTag": encrypted_value_data["tag"], - "secretCommentCiphertext": encrypted_comment_data["ciphertext"], - "secretCommentIV": encrypted_comment_data["iv"], - "secretCommentTag": encrypted_comment_data["tag"] - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -create_secrets() - -``` - - - - - - - Retrieve a secret from Infisical. -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const getSecret = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Get the secret from your project and environment - const { data } = await axios.get( - `${BASE_URL}/api/v3/secrets/${secretKey}?${new URLSearchParams({ - environment: serviceTokenData.environment, - workspaceId: serviceTokenData.workspace, - type: secretType // optional, defaults to 'shared' - })}`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - const encryptedSecret = data.secret; - - // 3. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 4. Decrypt the (encrypted) secret value - - const secretValue = decrypt({ - ciphertext: encryptedSecret.secretValueCiphertext, - iv: encryptedSecret.secretValueIV, - tag: encryptedSecret.secretValueTag, - secret: projectKey - }); - - console.log('secret: ', ({ - secretKey, - secretValue - })); -} - -getSecret(); - -``` - - - -```Python -import requests -import base64 -from Cryptodome.Cipher import AES - - -BASE_URL = "http://app.infisical.com" - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def get_secret(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Get secret from your project and environment - data = requests.get( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - params={ - "environment": service_token_data["environment"], - "workspaceId": service_token_data["workspace"], - "type": secret_type # optional, defaults to "shared" - }, - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - encrypted_secret = data["secret"] - - # 3. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 4. Decrypt the (encrypted) secret value - secret_value = decrypt( - ciphertext=encrypted_secret["secretValueCiphertext"], - iv=encrypted_secret["secretValueIV"], - tag=encrypted_secret["secretValueTag"], - secret=project_key, - ) - - print("secret: ", { - "secret_key": secret_key, - "secret_value": secret_value - }) - - -get_secret() - -``` - - - - - - -Update an existing secret in Infisical. -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; -const BLOCK_SIZE_BYTES = 16; - -const encrypt = ({ text, secret }) => { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); - return { - ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') - }; -} - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const updateSecrets = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key'; - const secretValue = 'updated_value'; - const secretComment = 'updated_comment'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 3. Encrypt your updated secret with the project key - const { - ciphertext: secretKeyCiphertext, - iv: secretKeyIV, - tag: secretKeyTag - } = encrypt({ - text: secretKey, - secret: projectKey - }); - - const { - ciphertext: secretValueCiphertext, - iv: secretValueIV, - tag: secretValueTag - } = encrypt({ - text: secretValue, - secret: projectKey - }); - - const { - ciphertext: secretCommentCiphertext, - iv: secretCommentIV, - tag: secretCommentTag - } = encrypt({ - text: secretComment, - secret: projectKey - }); - - // 4. Send (encrypted) updated secret to Infisical - await axios.patch( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); -} - -updateSecrets(); -``` - - - -```Python -import base64 -import requests -from Cryptodome.Cipher import AES -from Cryptodome.Random import get_random_bytes - - -BASE_URL = "https://app.infisical.com" -BLOCK_SIZE_BYTES = 16 - - -def encrypt(text, secret): - iv = get_random_bytes(BLOCK_SIZE_BYTES) - secret = bytes(secret, "utf-8") - cipher = AES.new(secret, AES.MODE_GCM, iv) - ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8")) - return { - "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"), - "tag": base64.standard_b64encode(tag).decode("utf-8"), - "iv": base64.standard_b64encode(iv).decode("utf-8"), - } - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def update_secret(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - secret_value = "updated_value" - secret_comment = "updated_comment" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 3. Encrypt your updated secret with the project key - encrypted_key_data = encrypt(text=secret_key, secret=project_key) - encrypted_value_data = encrypt(text=secret_value, secret=project_key) - encrypted_comment_data = encrypt(text=secret_comment, secret=project_key) - - # 4. Send (encrypted) updated secret to Infisical - requests.patch( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type, - "secretKeyCiphertext": encrypted_key_data["ciphertext"], - "secretKeyIV": encrypted_key_data["iv"], - "secretKeyTag": encrypted_key_data["tag"], - "secretValueCiphertext": encrypted_value_data["ciphertext"], - "secretValueIV": encrypted_value_data["iv"], - "secretValueTag": encrypted_value_data["tag"], - "secretCommentCiphertext": encrypted_comment_data["ciphertext"], - "secretCommentIV": encrypted_comment_data["iv"], - "secretCommentTag": encrypted_comment_data["tag"] - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -update_secret() - -``` - - - - - - - Delete a secret in Infisical. -```js -const axios = require('axios'); -const BASE_URL = 'https://app.infisical.com'; - -const deleteSecrets = async () => { - const serviceToken = 'your_service_token'; - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key' - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Delete secret from Infisical - await axios.delete( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - }, - } - ); -}; - -deleteSecrets(); -``` - - - -```Python -import requests - -BASE_URL = "https://app.infisical.com" - - -def delete_secrets(): - service_token = "" - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Delete secret from Infisical - requests.delete( - f"{BASE_URL}/api/v2/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -delete_secrets() - -``` - - - - If using an `API_KEY` to authenticate with the Infisical API, then you should include it in the `X_API_KEY` header. - - - - \ No newline at end of file diff --git a/docs/api-reference/overview/examples/integration.mdx b/docs/api-reference/overview/examples/integration.mdx new file mode 100644 index 000000000..e8da5ea49 --- /dev/null +++ b/docs/api-reference/overview/examples/integration.mdx @@ -0,0 +1,90 @@ +--- +title: "Configure native integrations programmatically" +description: "How to use Infisical API to sync secrets to external secret managers" +--- + +The Infisical API allows you to create programmatic integrations that connect with third-party secret managers to synchronize secrets from Infisical. + +This guide will primarily demonstrate the process using AWS Secret Store Manager (AWS SSM), but the steps are generally applicable to other secret management integrations. + + + For details on setting up AWS SSM synchronization and understanding its prerequisites, refer to the [AWS SSM integration setup documentation](../../../integrations/cloud/aws-secret-manager). + + + + + Authentication is required for all integrations. Use the [Integration Auth API](../../endpoints/integrations/create-auth) with the following parameters to authenticate. + + + Set this parameter to **aws-secret-manager**. + + + The Infisical project ID for the integration. + + + The AWS IAM User Access ID. + + + The AWS IAM User Access Secret Key. + + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/integration-auth/access-token \ + --header 'Authorization: ' \ + --header 'Content-Type: application/json' \ + --data '{ + "workspaceId": "", + "integration": "aws-secret-manager", + "accessId": "", + "accessToken": "" + }' + ``` + + + + Once authentication between AWS SSM and Infisical is established, you can configure the synchronization behavior. + This involves specifying the source (environment and secret path in Infisical) and the destination in SSM to which the secrets will be synchronized. + + Use the [integration API](../../endpoints/integrations/create) with the following parameters to configure the sync source and destination. + + + The ID of the integration authentication object used with AWS, obtained from the previous API response. + + + Indicates whether the integration should be active or inactive. + + + The secret name for saving in AWS SSM, which can be arbitrarily chosen. + + + The AWS region where the SSM is located, e.g., `us-east-1`. + + + The Infisical environment slug from which secrets will be synchronized, e.g., `dev`. + + + The Infisical folder path from which secrets will be synchronized, e.g., `/some/path`. The root path is `/`. + + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/integration \ + --header 'Authorization: ' \ + --header 'Content-Type: application/json' \ + --data '{ + "integrationAuthId": "", + "sourceEnvironment": "", + "secretPath": "", + "app": "", + "region": "" + }' + ``` + + + + + +Congratulations! You have successfully set up an integration to synchronize secrets from Infisical with AWS SSM. +For more information, [view the integration API reference](../../endpoints/integrations). + \ No newline at end of file diff --git a/docs/documentation/platform/access-controls/access-requests.mdx b/docs/documentation/platform/access-controls/access-requests.mdx index e8a52b0e4..45c155ab4 100644 --- a/docs/documentation/platform/access-controls/access-requests.mdx +++ b/docs/documentation/platform/access-controls/access-requests.mdx @@ -3,11 +3,20 @@ title: "Access Requests" description: "Learn how to request access to sensitive resources in Infisical." --- -In certain situations, developers need to expand their access to certain new project or a sensitive environment. For those use cases, it is helpful to utilize Infisical's **Access Requests** functionality. +In certain situations, developers need to expand their access to a certain new project or a sensitive environment. For those use cases, it is helpful to utilize Infisical's **Access Requests** functionality. This functionality works in the following way: -1. A project administrator sets up a policy that assigns access managers to a certain sensitive folder or environment. -2. When a developer requests access to one of such sensitive resources, corresponding access managers get an email notification about it. -3. An access manager can approve or deny the access request as well as specify the duration of access in the case of approval. -4. As soon as the request is approved, developer is able to access the sought resources. +1. A project administrator sets up a policy that assigns access managers (also known as eligible approvers) to a certain sensitive folder or environment. +![Create Access Request Policy Modal](/images/platform/access-controls/create-access-request-policy.png) +![Access Request Policies](/images/platform/access-controls/access-request-policies.png) + +2. When a developer requests access to one of such sensitive resources, the request is visible in the dashboard, and the corresponding eligible approvers get an email notification about it. +![Access Request Create](/images/platform/access-controls/request-access.png) +![Access Request Dashboard](/images/platform/access-controls/access-requests-pending.png) + +3. An eligible approver can approve or reject the access request. +![Access Request Review](/images/platform/access-controls/review-access-request.png) + +4. As soon as the request is approved, developer is able to access the sought resources. +![Access Request Dashboard](/images/platform/access-controls/access-requests-completed.png) diff --git a/docs/documentation/platform/sso/keycloak-saml.mdx b/docs/documentation/platform/sso/keycloak-saml.mdx new file mode 100644 index 000000000..981739711 --- /dev/null +++ b/docs/documentation/platform/sso/keycloak-saml.mdx @@ -0,0 +1,139 @@ +--- +title: "Keycloak SAML" +description: "Learn how to configure Keycloak SAML for Infisical SSO." +--- + + + Keycloak SAML SSO is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, + then you should contact sales@infisical.com to purchase an enterprise license to use it. + + + + + In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Manage**. + + ![Keycloak SAML organization security section](../../../images/sso/keycloak/org-security-section.png) + + Next, copy the **Valid redirect URI** and **SP Entity ID** to use when configuring the Keycloak SAML application. + + ![Keycloak SAML initial configuration](../../../images/sso/keycloak/init-config.png) + + + 2.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application. + + ![SAML keycloak list of clients](../../../images/sso/keycloak/clients-list.png) + + + You don’t typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm. + + + In the General Settings step, set **Client type** to **SAML**, the **Client ID** field to `https://app.infisical.com`, and the **Name** field to a friendly name like **Infisical**. + + ![SAML keycloak create client general settings](../../../images/sso/keycloak/create-client-general-settings.png) + + + If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com with your own domain. + + + Next, in the Login Settings step, set both the **Home URL** field and **Valid redirect URIs** field to the **Valid redirect URI** from step 1 and press **Save**. + + ![SAML keycloak create client login settings](../../../images/sso/keycloak/create-client-login-settings.png) + + 2.2. Once you've created the client, under its **Settings** tab, make sure to set the following values: + + - Under **SAML Capabilities**: + - Name ID format: email (or username). + - Force name ID format: On. + - Force POST binding: On. + - Include AuthnStatement: On. + - Under **Signature and Encryption**: + - Sign documents: On. + - Sign assertions: On. + - Signature algorithm: RSA_SHA256. + + ![SAML keycloak client SAML capabilities](../../../images/sso/keycloak/client-saml-capabilities.png) + + ![SAML keycloak client signature encryption](../../../images/sso/keycloak/client-signature-encryption.png) + + 2.3. Next, navigate to the **Client scopes** tab select the client's dedicated scope. + + ![SAML keycloak client scopes list](../../../images/sso/keycloak/client-scopes-list.png) + + Next click **Add predefined mapper**. + + ![SAML keycloak client mappers empty](../../../images/sso/keycloak/client-mappers-empty.png) + + Select the **X500 email**, **X500 givenName**, and **X500 surname** attributes and click **Add**. + + ![SAML keycloak client mappers predefined](../../../images/sso/keycloak/client-mappers-predefined.png) + + Now click on the **X500 email** mapper and set the **SAML Attribute Name** field to **email**. + + ![SAML keycloak client mappers email](../../../images/sso/keycloak/client-mappers-email.png) + + Repeat the same for **X500 givenName** and **X500 surname** mappers, setting the **SAML Attribute Name** field to **firstName** and **lastName** respectively. + + Next, back in the client scope's **Mappers**, click **Add mapper** and select **by configuration**. + + ![SAML keycloak client mappers by configuration](../../../images/sso/keycloak/client-mappers-by-configuration.png) + + Select **User Property**. + + ![SAML keycloak client mappers user property](../../../images/sso/keycloak/client-mappers-user-property.png) + + Set the the **Name** field to **Username**, the **Property** field to **username**, and the **SAML Attribtue Name** to **username**. + + ![SAML keycloak client mappers username](../../../images/sso/keycloak/client-mappers-username.png) + + Repeat the same for the `id` attribute, setting the **Name** field to **ID**, the **Property** field to **id**, and the **SAML Attribute Name** to **id**. + + ![SAML keycloak client mappers id](../../../images/sso/keycloak/client-mappers-id.png) + + Once you've completed the above steps, the list of mappers should look like this: + + ![SAML keycloak client mappers completed](../../../images/sso/keycloak/client-mappers-completed.png) + + + Back in Keycloak, navigate to Configure > Realm settings > General tab > Endpoints > SAML 2.0 Identity Provider Metadata and copy the IDP URL. This should appear in various places and take the form: `https://keycloak-mysite.com/realms/myrealm/protocol/saml`. + + ![SAML keycloak realm SAML metadata](../../../images/sso/keycloak/realm-saml-metadata.png) + + Also, in the **Keys** tab, locate the RS256 key and copy the certificate to use when finishing configuring Keycloak SAML in Infisical. + + ![SAML keycloak realm settings keys](../../../images/sso/keycloak/realm-settings-keys.png) + + + Back in Infisical, set **IDP URL** and **Certificate** to the items from step 3. Also, set the **Client ID** to the `https://app.infisical.com`. + + Once you've done that, press **Update** to complete the required configuration. + + ![SAML Okta paste values into Infisical](../../../images/sso/keycloak/idp-values.png) + + + Enabling SAML SSO allows members in your organization to log into Infisical via Keycloak. + + ![SAML keycloak enable SAML](../../../images/sso/keycloak/enable-saml.png) + + + Enforcing SAML SSO ensures that members in your organization can only access Infisical + by logging into the organization via Keycloak. + + To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Keycloak user with Infisical; + Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO. + + + We recommend ensuring that your account is provisioned the application in Keycloak + prior to enforcing SAML SSO to prevent any unintended issues. + + + + + + If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to + set the `AUTH_SECRET` and `SITE_URL` environment variable for it to work: + + - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`. + - `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com) + \ No newline at end of file diff --git a/docs/documentation/platform/sso/overview.mdx b/docs/documentation/platform/sso/overview.mdx index 42c2d986b..6064f26e8 100644 --- a/docs/documentation/platform/sso/overview.mdx +++ b/docs/documentation/platform/sso/overview.mdx @@ -15,11 +15,11 @@ description: "Learn how to log in to Infisical via SSO protocols." You can configure your organization in Infisical to have members authenticate with the platform via protocols like [SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0). To note, Infisical's SSO implementation decouples the **authentication** and **decryption** steps – which implies that no -Identitiy Provider can have access to the decryption key needed to decrypt your secrets (this also implies that Infisical requires entering the user's Master Password on top of authenticating with SSO). +Identity Provider can have access to the decryption key needed to decrypt your secrets (this also implies that Infisical requires entering the user's Master Password on top of authenticating with SSO). ## Identity providers -Infisical these and many other identity providers: +Infisical supports these and many other identity providers: - [Google SSO](/documentation/platform/sso/google) - [GitHub SSO](/documentation/platform/sso/github) @@ -27,6 +27,7 @@ Infisical these and many other identity providers: - [Okta SAML](/documentation/platform/sso/okta) - [Azure SAML](/documentation/platform/sso/azure) - [JumpCloud SAML](/documentation/platform/sso/jumpcloud) +- [Keycloak SAML](/documentation/platform/sso/keycloak-saml) - [Google SAML](/documentation/platform/sso/google-saml) If your required identity provider is not shown in the list above, please reach out to [team@infisical.com](mailto:team@infisical.com) for assistance. diff --git a/docs/documentation/platform/token.mdx b/docs/documentation/platform/token.mdx index efb1e05d4..78445f4f1 100644 --- a/docs/documentation/platform/token.mdx +++ b/docs/documentation/platform/token.mdx @@ -44,7 +44,7 @@ In the above screenshot, you can see that we are creating a token token with `re of the `/common` path within the development environment of the project; the token expires in 6 months and can be used from any IP address. -For a deeper understanding of service tokens, it is recommended to read [this guide](http://localhost:3000/internals/service-tokens). +For a deeper understanding of service tokens, it is recommended to read [this guide](https://infisical.com/docs/internals/service-tokens). **FAQ** diff --git a/docs/images/platform/access-controls/access-request-policies.png b/docs/images/platform/access-controls/access-request-policies.png new file mode 100644 index 000000000..d7ea4829c Binary files /dev/null and b/docs/images/platform/access-controls/access-request-policies.png differ diff --git a/docs/images/platform/access-controls/access-requests-completed.png b/docs/images/platform/access-controls/access-requests-completed.png new file mode 100644 index 000000000..a2a167f78 Binary files /dev/null and b/docs/images/platform/access-controls/access-requests-completed.png differ diff --git a/docs/images/platform/access-controls/access-requests-pending.png b/docs/images/platform/access-controls/access-requests-pending.png new file mode 100644 index 000000000..d75f669e2 Binary files /dev/null and b/docs/images/platform/access-controls/access-requests-pending.png differ diff --git a/docs/images/platform/access-controls/create-access-request-policy.png b/docs/images/platform/access-controls/create-access-request-policy.png new file mode 100644 index 000000000..6593fd733 Binary files /dev/null and b/docs/images/platform/access-controls/create-access-request-policy.png differ diff --git a/docs/images/platform/access-controls/request-access.png b/docs/images/platform/access-controls/request-access.png new file mode 100644 index 000000000..63c76dbd5 Binary files /dev/null and b/docs/images/platform/access-controls/request-access.png differ diff --git a/docs/images/platform/access-controls/review-access-request.png b/docs/images/platform/access-controls/review-access-request.png new file mode 100644 index 000000000..8376f9691 Binary files /dev/null and b/docs/images/platform/access-controls/review-access-request.png differ diff --git a/docs/images/sso/keycloak/client-mappers-by-configuration.png b/docs/images/sso/keycloak/client-mappers-by-configuration.png new file mode 100644 index 000000000..9bebb422e Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-by-configuration.png differ diff --git a/docs/images/sso/keycloak/client-mappers-completed.png b/docs/images/sso/keycloak/client-mappers-completed.png new file mode 100644 index 000000000..38fb82006 Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-completed.png differ diff --git a/docs/images/sso/keycloak/client-mappers-email.png b/docs/images/sso/keycloak/client-mappers-email.png new file mode 100644 index 000000000..e1a369bab Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-email.png differ diff --git a/docs/images/sso/keycloak/client-mappers-empty.png b/docs/images/sso/keycloak/client-mappers-empty.png new file mode 100644 index 000000000..01ec1d3e6 Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-empty.png differ diff --git a/docs/images/sso/keycloak/client-mappers-id.png b/docs/images/sso/keycloak/client-mappers-id.png new file mode 100644 index 000000000..a45638b87 Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-id.png differ diff --git a/docs/images/sso/keycloak/client-mappers-predefined.png b/docs/images/sso/keycloak/client-mappers-predefined.png new file mode 100644 index 000000000..750d600b7 Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-predefined.png differ diff --git a/docs/images/sso/keycloak/client-mappers-user-property.png b/docs/images/sso/keycloak/client-mappers-user-property.png new file mode 100644 index 000000000..c854f9521 Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-user-property.png differ diff --git a/docs/images/sso/keycloak/client-mappers-username.png b/docs/images/sso/keycloak/client-mappers-username.png new file mode 100644 index 000000000..ff2a8fc39 Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-username.png differ diff --git a/docs/images/sso/keycloak/client-saml-capabilities.png b/docs/images/sso/keycloak/client-saml-capabilities.png new file mode 100644 index 000000000..a4383628a Binary files /dev/null and b/docs/images/sso/keycloak/client-saml-capabilities.png differ diff --git a/docs/images/sso/keycloak/client-scopes-list.png b/docs/images/sso/keycloak/client-scopes-list.png new file mode 100644 index 000000000..16f908af4 Binary files /dev/null and b/docs/images/sso/keycloak/client-scopes-list.png differ diff --git a/docs/images/sso/keycloak/client-signature-encryption.png b/docs/images/sso/keycloak/client-signature-encryption.png new file mode 100644 index 000000000..b03b07a75 Binary files /dev/null and b/docs/images/sso/keycloak/client-signature-encryption.png differ diff --git a/docs/images/sso/keycloak/clients-list.png b/docs/images/sso/keycloak/clients-list.png new file mode 100644 index 000000000..ad05b2004 Binary files /dev/null and b/docs/images/sso/keycloak/clients-list.png differ diff --git a/docs/images/sso/keycloak/create-client-general-settings.png b/docs/images/sso/keycloak/create-client-general-settings.png new file mode 100644 index 000000000..866a92070 Binary files /dev/null and b/docs/images/sso/keycloak/create-client-general-settings.png differ diff --git a/docs/images/sso/keycloak/create-client-login-settings.png b/docs/images/sso/keycloak/create-client-login-settings.png new file mode 100644 index 000000000..6fa8b4ce4 Binary files /dev/null and b/docs/images/sso/keycloak/create-client-login-settings.png differ diff --git a/docs/images/sso/keycloak/enable-saml.png b/docs/images/sso/keycloak/enable-saml.png new file mode 100644 index 000000000..f66af968a Binary files /dev/null and b/docs/images/sso/keycloak/enable-saml.png differ diff --git a/docs/images/sso/keycloak/idp-values.png b/docs/images/sso/keycloak/idp-values.png new file mode 100644 index 000000000..9de14f23b Binary files /dev/null and b/docs/images/sso/keycloak/idp-values.png differ diff --git a/docs/images/sso/keycloak/init-config.png b/docs/images/sso/keycloak/init-config.png new file mode 100644 index 000000000..d500bb86e Binary files /dev/null and b/docs/images/sso/keycloak/init-config.png differ diff --git a/docs/images/sso/keycloak/org-security-section.png b/docs/images/sso/keycloak/org-security-section.png new file mode 100644 index 000000000..bbbfb2d42 Binary files /dev/null and b/docs/images/sso/keycloak/org-security-section.png differ diff --git a/docs/images/sso/keycloak/realm-saml-metadata.png b/docs/images/sso/keycloak/realm-saml-metadata.png new file mode 100644 index 000000000..c5ea5d497 Binary files /dev/null and b/docs/images/sso/keycloak/realm-saml-metadata.png differ diff --git a/docs/images/sso/keycloak/realm-settings-keys.png b/docs/images/sso/keycloak/realm-settings-keys.png new file mode 100644 index 000000000..3add94290 Binary files /dev/null and b/docs/images/sso/keycloak/realm-settings-keys.png differ diff --git a/docs/integrations/platforms/infisical-agent.mdx b/docs/integrations/platforms/infisical-agent.mdx index 3b0bd0bb1..1516ae045 100644 --- a/docs/integrations/platforms/infisical-agent.mdx +++ b/docs/integrations/platforms/infisical-agent.mdx @@ -52,7 +52,7 @@ While specifying an authentication method is mandatory to start the agent, confi | `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. | | `templates[].source-path` | The path to the template file that should be used to render secrets. | | `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. | -| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `60s` (optional) | +| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `5 minutes` (optional) | | `templates[].config.execute.command` | The command to execute when secret change is detected (optional) | | `templates[].config.execute.timeout` | How long in seconds to wait for command to execute before timing out (optional) | diff --git a/docs/mint.json b/docs/mint.json index 9d9759f0e..5f3836e49 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -32,7 +32,10 @@ "thumbsRating": true }, "api": { - "baseUrl": ["https://app.infisical.com", "http://localhost:8080"] + "baseUrl": [ + "https://app.infisical.com", + "http://localhost:8080" + ] }, "topbarLinks": [ { @@ -161,6 +164,7 @@ "documentation/platform/sso/okta", "documentation/platform/sso/azure", "documentation/platform/sso/jumpcloud", + "documentation/platform/sso/keycloak-saml", "documentation/platform/sso/google-saml" ] }, @@ -279,7 +283,7 @@ "integrations/cloud/azure-key-vault", "integrations/cloud/gcp-secret-manager", { - "group": "Cloudflare", + "group": "Cloudflare", "pages": [ "integrations/cloud/cloudflare-pages", "integrations/cloud/cloudflare-workers" @@ -356,14 +360,16 @@ }, { "group": "Build Tool Integrations", - "pages": ["integrations/build-tools/gradle"] + "pages": [ + "integrations/build-tools/gradle" + ] }, { "group": "", "pages": [ "sdks/overview" ] - }, + }, { "group": "SDK's", "pages": [ @@ -382,8 +388,7 @@ "group": "Examples", "pages": [ "api-reference/overview/examples/note", - "api-reference/overview/examples/e2ee-disabled", - "api-reference/overview/examples/e2ee-enabled" + "api-reference/overview/examples/integration" ] } ] @@ -466,7 +471,7 @@ ] }, { - "group": "Secret tags", + "group": "Secret Tags", "pages": [ "api-reference/endpoints/secret-tags/list", "api-reference/endpoints/secret-tags/create", @@ -486,7 +491,7 @@ ] }, { - "group": "Secret imports", + "group": "Secret Imports", "pages": [ "api-reference/endpoints/secret-imports/list", "api-reference/endpoints/secret-imports/create", @@ -494,13 +499,41 @@ "api-reference/endpoints/secret-imports/delete" ] }, + { + "group": "Identity Specific Privilege", + "pages": [ + "api-reference/endpoints/identity-specific-privilege/create-permanent", + "api-reference/endpoints/identity-specific-privilege/create-temporary", + "api-reference/endpoints/identity-specific-privilege/update", + "api-reference/endpoints/identity-specific-privilege/delete", + "api-reference/endpoints/identity-specific-privilege/find-by-slug", + "api-reference/endpoints/identity-specific-privilege/list" + ] + }, + { + "group": "Integrations", + "pages": [ + "api-reference/endpoints/integrations/create-auth", + "api-reference/endpoints/integrations/list-auth", + "api-reference/endpoints/integrations/find-auth", + "api-reference/endpoints/integrations/delete-auth", + "api-reference/endpoints/integrations/delete-auth-by-id", + "api-reference/endpoints/integrations/create", + "api-reference/endpoints/integrations/update", + "api-reference/endpoints/integrations/delete" + ] + }, { "group": "Service Tokens", - "pages": ["api-reference/endpoints/service-tokens/get"] + "pages": [ + "api-reference/endpoints/service-tokens/get" + ] }, { "group": "Audit Logs", - "pages": ["api-reference/endpoints/audit-logs/export-audit-log"] + "pages": [ + "api-reference/endpoints/audit-logs/export-audit-log" + ] } ] }, @@ -516,21 +549,22 @@ }, { "group": "", - "pages": ["changelog/overview"] + "pages": [ + "changelog/overview" + ] }, { "group": "Contributing", "pages": [ - { - "group": "Getting Started", - "pages": [ - "contributing/getting-started/overview", - "contributing/getting-started/code-of-conduct", - "contributing/getting-started/pull-requests", - "contributing/getting-started/faq" - - ] - }, + { + "group": "Getting Started", + "pages": [ + "contributing/getting-started/overview", + "contributing/getting-started/code-of-conduct", + "contributing/getting-started/pull-requests", + "contributing/getting-started/faq" + ] + }, { "group": "Contributing to platform", "pages": [ @@ -540,11 +574,11 @@ ] }, { - "group": "Contributing to SDK", - "pages": [ - "contributing/sdk/developing" - ] - } + "group": "Contributing to SDK", + "pages": [ + "contributing/sdk/developing" + ] + } ] } ], diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index dae2e8044..db8034199 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -335,6 +335,10 @@ To login into Infisical with OAuth providers such as Google, configure the assoc Requires enterprise license. Please contact team@infisical.com to get more information. + + Configure SAML organization slug to automatically redirect all users of your Infisical instance to the identity provider. + + diff --git a/docs/self-hosting/configuration/requirements.mdx b/docs/self-hosting/configuration/requirements.mdx index 40fd80246..2e31ac853 100644 --- a/docs/self-hosting/configuration/requirements.mdx +++ b/docs/self-hosting/configuration/requirements.mdx @@ -47,7 +47,7 @@ Recommended minimum memory hardware for different sizes of deployments: PostgreSQL is the only database supported by Infisical. Infisical has been extensively tested with Postgres version 16. We recommend using versions 14 and up for optimal compatibility. Recommended resource allocation based on deployment size: -- **small:** 1 vCPU / 2 GB RAM / 10 GB Disk +- **small:** 2 vCPU / 8 GB RAM / 20 GB Disk - **large:** 4vCPU / 16 GB RAM / 100 GB Disk ### Redis @@ -58,7 +58,7 @@ Redis requirements: - Use Redis versions 6.x or 7.x. We advise upgrading to at least Redis 6.2. - Redis Cluster mode is currently not supported; use Redis Standalone, with or without High Availability (HA). -- Redis storage needs are minimal: a setup with 1 vCPU, 1 GB RAM, and 1GB SSD will be sufficient for small deployments. +- Redis storage needs are minimal: a setup with 2 vCPU, 4 GB RAM, and 30GB SSD will be sufficient for small deployments. ## Supported Web Browsers diff --git a/docs/self-hosting/deployment-options/docker-compose.mdx b/docs/self-hosting/deployment-options/docker-compose.mdx index f947610cf..291a91370 100644 --- a/docs/self-hosting/deployment-options/docker-compose.mdx +++ b/docs/self-hosting/deployment-options/docker-compose.mdx @@ -80,4 +80,4 @@ docker-compose -f docker-compose.prod.yml up Your Infisical instance should now be running on port `80`. To access your instance, visit `http://localhost:80`. -![self host sign up](images/self-hosting/applicable-to-all/selfhost-signup.png) \ No newline at end of file +![self host sign up](/images/self-hosting/applicable-to-all/selfhost-signup.png) \ No newline at end of file diff --git a/docs/self-hosting/deployment-options/standalone-infisical.mdx b/docs/self-hosting/deployment-options/standalone-infisical.mdx index 82086230c..ab1512612 100644 --- a/docs/self-hosting/deployment-options/standalone-infisical.mdx +++ b/docs/self-hosting/deployment-options/standalone-infisical.mdx @@ -52,7 +52,7 @@ The following guide provides a detailed step-by-step walkthrough on how you can Once the container is running, verify the installation by opening your web browser and navigating to `http://localhost:80`. - ![self host sign up](images/self-hosting/applicable-to-all/selfhost-signup.png) + ![self host sign up](/images/self-hosting/applicable-to-all/selfhost-signup.png) diff --git a/docs/style.css b/docs/style.css index 6674ce2c6..b76d06450 100644 --- a/docs/style.css +++ b/docs/style.css @@ -63,6 +63,30 @@ border-color: #ebebeb; } +#content-area .mt-8 .rounded-xl{ + border-radius: 0; +} + +#content-area .mt-8 .rounded-lg{ + border-radius: 0; +} + +#content-area .mt-6 .rounded-xl{ + border-radius: 0; +} + +#content-area .mt-6 .rounded-lg{ + border-radius: 0; +} + +#content-area .mt-6 .rounded-md{ + border-radius: 0; +} + +#content-area .mt-8 .rounded-md{ + border-radius: 0; +} + #content-area div.my-4{ border-radius: 0; border-width: 1px; @@ -78,6 +102,10 @@ border-radius: 0; } +#content-area a { + border-radius: 0; +} + #content-area .not-prose { border-radius: 0; } diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 2060c214a..9090fc603 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -52,6 +52,9 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ ARG INTERCOM_ID ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID +ARG SAML_ORG_SLUG +ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \ + BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG ARG NEXT_INFISICAL_PLATFORM_VERSION ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION=$NEXT_INFISICAL_PLATFORM_VERSION diff --git a/frontend/scripts/initialize-standalone-build.sh b/frontend/scripts/initialize-standalone-build.sh index d9138bb77..859814eda 100755 --- a/frontend/scripts/initialize-standalone-build.sh +++ b/frontend/scripts/initialize-standalone-build.sh @@ -4,6 +4,8 @@ scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID" +scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG" + if [ "$TELEMETRY_ENABLED" != "false" ]; then echo "Telemetry is enabled" scripts/set-standalone-build-telemetry.sh true diff --git a/frontend/scripts/start.sh b/frontend/scripts/start.sh index 1db867e15..1488ad328 100644 --- a/frontend/scripts/start.sh +++ b/frontend/scripts/start.sh @@ -4,6 +4,8 @@ scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY" "$NEXT_PUBLIC_P scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID" +scripts/replace-variable.sh "$BAKED_NEXT_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG" + if [ "$TELEMETRY_ENABLED" != "false" ]; then echo "Telemetry is enabled" scripts/set-telemetry.sh true diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 23f4dda4c..845030e9f 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -8,6 +8,7 @@ export enum AuthMethod { OKTA_SAML = "okta-saml", AZURE_SAML = "azure-saml", JUMPCLOUD_SAML = "jumpcloud-saml", + KEYCLOAK_SAML = "keycloak-saml", LDAP = "ldap" } diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index d2b41abcd..b6cc4ee46 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -483,10 +483,10 @@ const OrganizationPage = withPermission( const addUsersToProject = useAddUserToWsNonE2EE(); - const { data: updateClosed } = useGetUserAction("april_2024_db_update_closed"); + const { data: updateClosed } = useGetUserAction("april_13_2024_db_update_closed"); const registerUserAction = useRegisterUserAction(); const closeUpdate = async () => { - await registerUserAction.mutateAsync("april_2024_db_update_closed"); + await registerUserAction.mutateAsync("april_13_2024_db_update_closed"); }; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -594,6 +594,7 @@ const OrganizationPage = withPermission( )}
+ {window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("http://localhost:8080") && (
- Scheduled maintenance on April 6th 2024 {" "} + Scheduled maintenance on April 13th 2024 {" "}
- Infisical will undergo scheduled maintenance for approximately 1 hour on Saturday, April 6th, 11am EST. During these hours, read + Infisical will undergo scheduled maintenance for approximately 1 hour on Saturday, April 13th, 11am EST. During these hours, read operations will continue to function normally but no resources will be editable. - No action is required on your end — your applications can continue to fetch secrets. + No action is required on your end — your applications will continue to fetch secrets.
-
+
)} +

Projects

{ + if (process.env.NEXT_PUBLIC_SAML_ORG_SLUG && process.env.NEXT_PUBLIC_SAML_ORG_SLUG !== "saml-org-slug-default") { + const callbackPort = queryParams.get("callback_port"); + window.open( + `/api/v1/sso/redirect/saml2/organizations/${process.env.NEXT_PUBLIC_SAML_ORG_SLUG}${ + callbackPort ? `?callback_port=${callbackPort}` : "" + }` + ); + window.close(); + } + }, []) + const handleLogin = async (e: FormEvent) => { e.preventDefault(); try { @@ -244,7 +256,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
) : ( -
+
)}
diff --git a/frontend/src/views/Org/MembersPage/MembersPage.tsx b/frontend/src/views/Org/MembersPage/MembersPage.tsx index 673dc28c3..ccc868c0f 100644 --- a/frontend/src/views/Org/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Org/MembersPage/MembersPage.tsx @@ -23,9 +23,6 @@ export const MembersPage = withPermission(

Machine Identities

-
- New -
Organization Roles diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx index e2086a6ab..b4e08f51a 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -31,7 +31,7 @@ export const OrgRoleTable = ({ onSelectRole }: Props) => { const [searchRoles, setSearchRoles] = useState(""); const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; - + const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const); const { data: roles, isLoading: isRolesLoading } = useGetOrgRoles(orgId); @@ -49,7 +49,7 @@ export const OrgRoleTable = ({ onSelectRole }: Props) => { handlePopUpClose("deleteRole"); } catch (err) { console.log(err); - createNotification({ type: "error", text: "Failed to create role" }); + createNotification({ type: "error", text: "Failed to delete role" }); } }; diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx index 5855cc21d..ca7e31464 100644 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx @@ -338,9 +338,10 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal type="submit" className={twMerge( "transition-all", - "opacity-0", - roleForm.formState.isDirty && "opacity-100" + "opacity-0 cursor-default", + roleForm.formState.isDirty && "cursor-pointer opacity-100" )} + isDisabled={!roleForm.formState.isDirty} isLoading={roleForm.formState.isSubmitting} > Save Roles diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx index fa9d8ae0f..04497c62c 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx @@ -335,9 +335,10 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props) type="submit" className={twMerge( "transition-all", - "opacity-0", - roleForm.formState.isDirty && "opacity-100" + "opacity-0 cursor-default", + roleForm.formState.isDirty && "cursor-pointer opacity-100" )} + isDisabled={!roleForm.formState.isDirty} isLoading={roleForm.formState.isSubmitting} > Save Roles diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index aa29a363b..d8a22bdb4 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -29,7 +29,7 @@ type Props = { export const ProjectRoleList = ({ onSelectRole }: Props) => { const [searchRoles, setSearchRoles] = useState(""); - + const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const); const { currentWorkspace } = useWorkspace(); const workspaceId = currentWorkspace?.id || ""; @@ -50,7 +50,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => { handlePopUpClose("deleteRole"); } catch (err) { console.log(err); - createNotification({ type: "error", text: "Failed to create role" }); + createNotification({ type: "error", text: "Failed to delete role" }); } }; diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.tsx b/frontend/src/views/SecretMainPage/SecretMainPage.tsx index 270fd8c65..122135c03 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.tsx +++ b/frontend/src/views/SecretMainPage/SecretMainPage.tsx @@ -313,6 +313,7 @@ export const SecretMainPage = () => { workspaceId={workspaceId} secretPath={secretPath} sortDir={sortDir} + searchTerm={filter.searchFilter} /> {canReadSecret && ( { @@ -35,8 +37,6 @@ export const FolderListView = ({ ] as const); const router = useRouter(); - - const { mutateAsync: updateFolder } = useUpdateFolder(); const { mutateAsync: deleteFolder } = useDeleteFolder(); @@ -100,6 +100,7 @@ export const FolderListView = ({ return ( <> {folders + .filter(({ name }) => name.toUpperCase().includes(String(searchTerm?.toUpperCase()))) .sort((a, b) => sortDir === SortDir.ASC ? a.name.toLowerCase().localeCompare(b.name.toLowerCase()) diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/SSOModal.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/SSOModal.tsx index 4cad75ebd..23f5fab88 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/SSOModal.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/SSOModal.tsx @@ -22,6 +22,7 @@ enum AuthProvider { OKTA_SAML = "okta-saml", AZURE_SAML = "azure-saml", JUMPCLOUD_SAML = "jumpcloud-saml", + KEYCLOAK_SAML = "keycloak-saml", GOOGLE_SAML = "google-saml" } @@ -29,6 +30,7 @@ const ssoAuthProviders = [ { label: "Okta SAML", value: AuthProvider.OKTA_SAML }, { label: "Azure SAML", value: AuthProvider.AZURE_SAML }, { label: "JumpCloud SAML", value: AuthProvider.JUMPCLOUD_SAML }, + { label: "Keycloak SAML", value: AuthProvider.KEYCLOAK_SAML }, { label: "Google SAML", value: AuthProvider.GOOGLE_SAML } ]; @@ -142,6 +144,15 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) issuer: "IdP Entity ID", issuerPlaceholder: "xxx" }; + case AuthProvider.KEYCLOAK_SAML: + return { + acsUrl: "Valid redirect URI", + entityId: "SP Entity ID", + entryPoint: "IDP URL", + entryPointPlaceholder: "https://keycloak.mysite.com/realms/myrealm/protocol/saml", + issuer: "Client ID", + issuerPlaceholder: window.origin + }; case AuthProvider.GOOGLE_SAML: return { acsUrl: "ACS URL", diff --git a/k8-operator/README.md b/k8-operator/README.md index 807476c0c..f2e349f07 100644 --- a/k8-operator/README.md +++ b/k8-operator/README.md @@ -72,6 +72,12 @@ If you are editing the API definitions, generate the manifests such as CRs or CR make manifests ``` +Also, after editing the API definitions, update the kubectl-install folder: + +```sh +make kubectl-install +``` + **NOTE:** Run `make --help` for more information on all potential `make` targets More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) diff --git a/k8-operator/kubectl-install/install-secrets-operator.yaml b/k8-operator/kubectl-install/install-secrets-operator.yaml index 5a11db30a..4e66a03cd 100644 --- a/k8-operator/kubectl-install/install-secrets-operator.yaml +++ b/k8-operator/kubectl-install/install-secrets-operator.yaml @@ -96,12 +96,47 @@ spec: - secretsScope - serviceTokenSecretReference type: object + universalAuth: + properties: + credentialsRef: + properties: + secretName: + description: The name of the Kubernetes Secret + type: string + secretNamespace: + description: The name space where the Kubernetes Secret is located + type: string + required: + - secretName + - secretNamespace + type: object + secretsScope: + properties: + envSlug: + type: string + projectSlug: + type: string + secretsPath: + type: string + required: + - envSlug + - projectSlug + - secretsPath + type: object + required: + - credentialsRef + - secretsScope + type: object type: object hostAPI: description: Infisical host to pull secrets from type: string managedSecretReference: properties: + creationPolicy: + default: Orphan + description: 'The Kubernetes Secret creation policy. Enum with values: ''Owner'', ''Orphan''. Owner creates the secret and sets .metadata.ownerReferences of the InfisicalSecret CRD that created it. Orphan will not set the secret owner. This will result in the secret being orphaned and not deleted when the resource is deleted.' + type: string secretName: description: The name of the Kubernetes Secret type: string