diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index c386b132e..3fe0fbe21 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -145,3 +145,9 @@ jobs: INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }} + - name: Invalidate Cloudfront cache + run: aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths '/deb/dists/stable/*' + env: + AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }} + CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID }} diff --git a/README.md b/README.md index e79517cf8..f1393495d 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ We're on a mission to make security tooling more accessible to everyone, not jus - **[Dashboard](https://infisical.com/docs/documentation/platform/project)**: Manage secrets across projects and environments (e.g. development, production, etc.) through a user-friendly interface. - **[Native Integrations](https://infisical.com/docs/integrations/overview)**: Sync secrets to platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and use tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more. - **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)**: Keep track of every secret and project state; roll back when needed. -- **[Secret Rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview)**: Rotate secrets at regular intervals for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/secret-rotation/postgres), [MySQL](https://infisical.com/docs/documentation/platform/secret-rotation/mysql), [AWS IAM](https://infisical.com/docs/documentation/platform/secret-rotation/aws-iam), and more. +- **[Secret Rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview)**: Rotate secrets at regular intervals for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/secret-rotation/postgres-credentials), [MySQL](https://infisical.com/docs/documentation/platform/secret-rotation/mysql), [AWS IAM](https://infisical.com/docs/documentation/platform/secret-rotation/aws-iam), and more. - **[Dynamic Secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview)**: Generate ephemeral secrets on-demand for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/postgresql), [MySQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/mysql), [RabbitMQ](https://infisical.com/docs/documentation/platform/dynamic-secrets/rabbit-mq), and more. - **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)**: Prevent secrets from leaking to git. - **[Infisical Kubernetes Operator](https://infisical.com/docs/documentation/getting-started/kubernetes)**: Deliver secrets to your Kubernetes workloads and automatically reload deployments. diff --git a/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts b/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts new file mode 100644 index 000000000..9c5b4e730 --- /dev/null +++ b/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts @@ -0,0 +1,27 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "expiryNotificationSent"); + if (!hasCol) { + await knex.schema.alterTable(TableName.ServiceToken, (t) => { + t.boolean("expiryNotificationSent").defaultTo(false); + }); + + // Update only tokens where expiresAt is before current time + await knex(TableName.ServiceToken) + .whereRaw(`${TableName.ServiceToken}."expiresAt" < NOW()`) + .whereNotNull("expiresAt") + .update({ expiryNotificationSent: true }); + } +} + +export async function down(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "expiryNotificationSent"); + if (hasCol) { + await knex.schema.alterTable(TableName.ServiceToken, (t) => { + t.dropColumn("expiryNotificationSent"); + }); + } +} diff --git a/backend/src/db/migrations/20250414234624_add-project-delete-protection.ts b/backend/src/db/migrations/20250414234624_add-project-delete-protection.ts new file mode 100644 index 000000000..bf5e438e1 --- /dev/null +++ b/backend/src/db/migrations/20250414234624_add-project-delete-protection.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.Project, "hasDeleteProtection"); + if (!hasCol) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.boolean("hasDeleteProtection").defaultTo(true); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.Project, "hasDeleteProtection"); + if (hasCol) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.dropColumn("hasDeleteProtection"); + }); + } +} diff --git a/backend/src/db/migrations/20250416113437_add-oidc-jwt-signature-algorithm.ts b/backend/src/db/migrations/20250416113437_add-oidc-jwt-signature-algorithm.ts new file mode 100644 index 000000000..5adbf71ec --- /dev/null +++ b/backend/src/db/migrations/20250416113437_add-oidc-jwt-signature-algorithm.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.OidcConfig, "jwtSignatureAlgorithm"))) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + t.string("jwtSignatureAlgorithm").defaultTo(OIDCJWTSignatureAlgorithm.RS256).notNullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.OidcConfig, "jwtSignatureAlgorithm")) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + t.dropColumn("jwtSignatureAlgorithm"); + }); + } +} diff --git a/backend/src/db/migrations/20250416145120_add-enable-bypass-org-auth-flag.ts b/backend/src/db/migrations/20250416145120_add-enable-bypass-org-auth-flag.ts new file mode 100644 index 000000000..fb9a12625 --- /dev/null +++ b/backend/src/db/migrations/20250416145120_add-enable-bypass-org-auth-flag.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.Organization, "bypassOrgAuthEnabled"))) { + await knex.schema.alterTable(TableName.Organization, (t) => { + t.boolean("bypassOrgAuthEnabled").defaultTo(false).notNullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.Organization, "bypassOrgAuthEnabled")) { + await knex.schema.alterTable(TableName.Organization, (t) => { + t.dropColumn("bypassOrgAuthEnabled"); + }); + } +} diff --git a/backend/src/db/schemas/oidc-configs.ts b/backend/src/db/schemas/oidc-configs.ts index 76923aee8..181df25f0 100644 --- a/backend/src/db/schemas/oidc-configs.ts +++ b/backend/src/db/schemas/oidc-configs.ts @@ -30,9 +30,10 @@ export const OidcConfigsSchema = z.object({ updatedAt: z.date(), orgId: z.string().uuid(), lastUsed: z.date().nullable().optional(), - manageGroupMemberships: z.boolean().default(false), encryptedOidcClientId: zodBuffer, - encryptedOidcClientSecret: zodBuffer + encryptedOidcClientSecret: zodBuffer, + manageGroupMemberships: z.boolean().default(false), + jwtSignatureAlgorithm: z.string().default("RS256") }); export type TOidcConfigs = z.infer; diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index a18e258c7..eea1808e0 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -26,7 +26,8 @@ export const OrganizationsSchema = z.object({ allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(), shouldUseNewPrivilegeSystem: z.boolean().default(true), privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(), - privilegeUpgradeInitiatedAt: z.date().nullable().optional() + privilegeUpgradeInitiatedAt: z.date().nullable().optional(), + bypassOrgAuthEnabled: z.boolean().default(false) }); export type TOrganizations = z.infer; diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 8c1a0386c..2403d6cf4 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -26,7 +26,8 @@ export const ProjectsSchema = z.object({ kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(), description: z.string().nullable().optional(), type: z.string(), - enforceCapitalization: z.boolean().default(false) + enforceCapitalization: z.boolean().default(false), + hasDeleteProtection: z.boolean().default(true).nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/db/schemas/service-tokens.ts b/backend/src/db/schemas/service-tokens.ts index 720c8fd6f..8ffddb10a 100644 --- a/backend/src/db/schemas/service-tokens.ts +++ b/backend/src/db/schemas/service-tokens.ts @@ -21,7 +21,8 @@ export const ServiceTokensSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), createdBy: z.string(), - projectId: z.string() + projectId: z.string(), + expiryNotificationSent: z.boolean().default(false).nullable().optional() }); export type TServiceTokens = z.infer; diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts index 66bced3df..1bfc4d696 100644 --- a/backend/src/ee/routes/v1/oidc-router.ts +++ b/backend/src/ee/routes/v1/oidc-router.ts @@ -12,7 +12,7 @@ import RedisStore from "connect-redis"; import { z } from "zod"; import { OidcConfigsSchema } from "@app/db/schemas"; -import { OIDCConfigurationType } from "@app/ee/services/oidc/oidc-config-types"; +import { OIDCConfigurationType, OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types"; import { getConfig } from "@app/lib/config/env"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -30,7 +30,8 @@ const SanitizedOidcConfigSchema = OidcConfigsSchema.pick({ orgId: true, isActive: true, allowedEmailDomains: true, - manageGroupMemberships: true + manageGroupMemberships: true, + jwtSignatureAlgorithm: true }); export const registerOidcRouter = async (server: FastifyZodProvider) => { @@ -170,7 +171,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { isActive: true, orgId: true, allowedEmailDomains: true, - manageGroupMemberships: true + manageGroupMemberships: true, + jwtSignatureAlgorithm: true }).extend({ clientId: z.string(), clientSecret: z.string() @@ -225,7 +227,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { clientId: z.string().trim(), clientSecret: z.string().trim(), isActive: z.boolean(), - manageGroupMemberships: z.boolean().optional() + manageGroupMemberships: z.boolean().optional(), + jwtSignatureAlgorithm: z.nativeEnum(OIDCJWTSignatureAlgorithm).optional() }) .partial() .merge(z.object({ orgSlug: z.string() })), @@ -292,7 +295,11 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { clientSecret: z.string().trim(), isActive: z.boolean(), orgSlug: z.string().trim(), - manageGroupMemberships: z.boolean().optional().default(false) + manageGroupMemberships: z.boolean().optional().default(false), + jwtSignatureAlgorithm: z + .nativeEnum(OIDCJWTSignatureAlgorithm) + .optional() + .default(OIDCJWTSignatureAlgorithm.RS256) }) .superRefine((data, ctx) => { if (data.configurationType === OIDCConfigurationType.CUSTOM) { diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/auth0-client-secret-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/auth0-client-secret-rotation-router.ts new file mode 100644 index 000000000..6bcf1ea5e --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/auth0-client-secret-rotation-router.ts @@ -0,0 +1,19 @@ +import { + Auth0ClientSecretRotationGeneratedCredentialsSchema, + Auth0ClientSecretRotationSchema, + CreateAuth0ClientSecretRotationSchema, + UpdateAuth0ClientSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/auth0-client-secret"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; + +import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints"; + +export const registerAuth0ClientSecretRotationRouter = async (server: FastifyZodProvider) => + registerSecretRotationEndpoints({ + type: SecretRotation.Auth0ClientSecret, + server, + responseSchema: Auth0ClientSecretRotationSchema, + createSchema: CreateAuth0ClientSecretRotationSchema, + updateSchema: UpdateAuth0ClientSecretRotationSchema, + generatedCredentialsSchema: Auth0ClientSecretRotationGeneratedCredentialsSchema + }); diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts index d641ec689..1dacf1bd2 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts @@ -1,5 +1,6 @@ import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router"; import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router"; import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router"; @@ -10,5 +11,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record< (server: FastifyZodProvider) => Promise > = { [SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter, - [SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter + [SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter, + [SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter }; diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts index abdfc14f6..bfb8b38c0 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret"; import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; @@ -11,7 +12,8 @@ import { AuthMode } from "@app/services/auth/auth-type"; const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [ PostgresCredentialsRotationListItemSchema, - MsSqlCredentialsRotationListItemSchema + MsSqlCredentialsRotationListItemSchema, + Auth0ClientSecretRotationListItemSchema ]); export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 52c8dd597..adfe92341 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -165,7 +165,8 @@ export const oidcConfigServiceFactory = ({ allowedEmailDomains: oidcCfg.allowedEmailDomains, clientId, clientSecret, - manageGroupMemberships: oidcCfg.manageGroupMemberships + manageGroupMemberships: oidcCfg.manageGroupMemberships, + jwtSignatureAlgorithm: oidcCfg.jwtSignatureAlgorithm }; }; @@ -481,7 +482,8 @@ export const oidcConfigServiceFactory = ({ userinfoEndpoint, clientId, clientSecret, - manageGroupMemberships + manageGroupMemberships, + jwtSignatureAlgorithm }: TUpdateOidcCfgDTO) => { const org = await orgDAL.findOne({ slug: orgSlug @@ -536,7 +538,8 @@ export const oidcConfigServiceFactory = ({ jwksUri, isActive, lastUsed: null, - manageGroupMemberships + manageGroupMemberships, + jwtSignatureAlgorithm }; if (clientId !== undefined) { @@ -569,7 +572,8 @@ export const oidcConfigServiceFactory = ({ userinfoEndpoint, clientId, clientSecret, - manageGroupMemberships + manageGroupMemberships, + jwtSignatureAlgorithm }: TCreateOidcCfgDTO) => { const org = await orgDAL.findOne({ slug: orgSlug @@ -613,6 +617,7 @@ export const oidcConfigServiceFactory = ({ userinfoEndpoint, orgId: org.id, manageGroupMemberships, + jwtSignatureAlgorithm, encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob, encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob }); @@ -676,7 +681,8 @@ export const oidcConfigServiceFactory = ({ const client = new issuer.Client({ client_id: oidcCfg.clientId, client_secret: oidcCfg.clientSecret, - redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`] + redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`], + id_token_signed_response_alg: oidcCfg.jwtSignatureAlgorithm }); const strategy = new OpenIdStrategy( diff --git a/backend/src/ee/services/oidc/oidc-config-types.ts b/backend/src/ee/services/oidc/oidc-config-types.ts index a6bd6ad67..3b2194375 100644 --- a/backend/src/ee/services/oidc/oidc-config-types.ts +++ b/backend/src/ee/services/oidc/oidc-config-types.ts @@ -5,6 +5,12 @@ export enum OIDCConfigurationType { DISCOVERY_URL = "discoveryURL" } +export enum OIDCJWTSignatureAlgorithm { + RS256 = "RS256", + HS256 = "HS256", + RS512 = "RS512" +} + export type TOidcLoginDTO = { externalId: string; email: string; @@ -40,6 +46,7 @@ export type TCreateOidcCfgDTO = { isActive: boolean; orgSlug: string; manageGroupMemberships: boolean; + jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm; } & TGenericPermission; export type TUpdateOidcCfgDTO = Partial<{ @@ -56,5 +63,6 @@ export type TUpdateOidcCfgDTO = Partial<{ isActive: boolean; orgSlug: string; manageGroupMemberships: boolean; + jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm; }> & TGenericPermission; diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index 9b23d6113..891d7193e 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { TDbClient } from "@app/db"; import { IdentityProjectMembershipRoleSchema, + OrgMembershipRole, OrgMembershipsSchema, TableName, TProjectRoles, @@ -53,6 +54,7 @@ export const permissionDALFactory = (db: TDbClient) => { db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"), db.ref("permissions").withSchema(TableName.OrgRoles), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), + db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"), db.ref("groupId").withSchema("userGroups"), db.ref("groupOrgId").withSchema("userGroups"), db.ref("groupName").withSchema("userGroups"), @@ -71,6 +73,7 @@ export const permissionDALFactory = (db: TDbClient) => { OrgMembershipsSchema.extend({ permissions: z.unknown(), orgAuthEnforced: z.boolean().optional().nullable(), + bypassOrgAuthEnabled: z.boolean(), customRoleSlug: z.string().optional().nullable(), shouldUseNewPrivilegeSystem: z.boolean() }).parse(el), @@ -571,6 +574,11 @@ export const permissionDALFactory = (db: TDbClient) => { }) .join(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId])) .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) + .join(TableName.OrgMembership, (qb) => { + void qb + .on(`${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .andOn(`${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`); + }) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder .on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`) @@ -670,6 +678,8 @@ export const permissionDALFactory = (db: TDbClient) => { db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), + db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"), + db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"), db.ref("orgId").withSchema(TableName.Project), db.ref("type").withSchema(TableName.Project).as("projectType"), db.ref("id").withSchema(TableName.Project).as("projectId"), @@ -683,6 +693,7 @@ export const permissionDALFactory = (db: TDbClient) => { orgId, username, orgAuthEnforced, + orgRole, membershipId, groupMembershipId, membershipCreatedAt, @@ -690,10 +701,12 @@ export const permissionDALFactory = (db: TDbClient) => { groupMembershipUpdatedAt, membershipUpdatedAt, projectType, - shouldUseNewPrivilegeSystem + shouldUseNewPrivilegeSystem, + bypassOrgAuthEnabled }) => ({ orgId, orgAuthEnforced, + orgRole: orgRole as OrgMembershipRole, userId, projectId, username, @@ -701,7 +714,8 @@ export const permissionDALFactory = (db: TDbClient) => { id: membershipId || groupMembershipId, createdAt: membershipCreatedAt || groupMembershipCreatedAt, updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt, - shouldUseNewPrivilegeSystem + shouldUseNewPrivilegeSystem, + bypassOrgAuthEnabled }), childrenMapper: [ { diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 88bace1f0..d645e2bec 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -2,7 +2,7 @@ import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability"; import { z } from "zod"; -import { TOrganizations } from "@app/db/schemas"; +import { OrgMembershipRole, TOrganizations } from "@app/db/schemas"; import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; @@ -118,11 +118,20 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { ].includes(actorAuthMethod); } -function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"]) { +function validateOrgSSO( + actorAuthMethod: ActorAuthMethod, + isOrgSsoEnforced: TOrganizations["authEnforced"], + isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"], + orgRole: OrgMembershipRole +) { if (actorAuthMethod === undefined) { throw new UnauthorizedError({ name: "No auth method defined" }); } + if (isOrgSsoEnforced && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) { + return; + } + if ( isOrgSsoEnforced && actorAuthMethod !== null && diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 299f509d7..0082c3d17 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -139,7 +139,12 @@ export const permissionServiceFactory = ({ throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); } - validateOrgSSO(authMethod, membership.orgAuthEnforced); + validateOrgSSO( + authMethod, + membership.orgAuthEnforced, + membership.bypassOrgAuthEnabled, + membership.role as OrgMembershipRole + ); const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat( membership?.groups?.map(({ role, customRolePermission }) => ({ @@ -226,7 +231,12 @@ export const permissionServiceFactory = ({ throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); } - validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced); + validateOrgSSO( + authMethod, + userProjectPermission.orgAuthEnforced, + userProjectPermission.bypassOrgAuthEnabled, + userProjectPermission.orgRole + ); if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) { throw new BadRequestError({ diff --git a/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-constants.ts new file mode 100644 index 000000000..a4d0a956c --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-constants.ts @@ -0,0 +1,15 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = { + name: "Auth0 Client Secret", + type: SecretRotation.Auth0ClientSecret, + connection: AppConnection.Auth0, + template: { + secretsMapping: { + clientId: "AUTH0_CLIENT_ID", + clientSecret: "AUTH0_CLIENT_SECRET" + } + } +}; diff --git a/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns.ts new file mode 100644 index 000000000..6debd2402 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns.ts @@ -0,0 +1,104 @@ +import { + TAuth0ClientSecretRotationGeneratedCredentials, + TAuth0ClientSecretRotationWithConnection +} from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-types"; +import { + TRotationFactory, + TRotationFactoryGetSecretsPayload, + TRotationFactoryIssueCredentials, + TRotationFactoryRevokeCredentials, + TRotationFactoryRotateCredentials +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { request } from "@app/lib/config/request"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { getAuth0ConnectionAccessToken } from "@app/services/app-connection/auth0"; + +import { generatePassword } from "../shared/utils"; + +export const auth0ClientSecretRotationFactory: TRotationFactory< + TAuth0ClientSecretRotationWithConnection, + TAuth0ClientSecretRotationGeneratedCredentials +> = (secretRotation, appConnectionDAL, kmsService) => { + const { + connection, + parameters: { clientId }, + secretsMapping + } = secretRotation; + + const $rotateClientSecret = async () => { + const accessToken = await getAuth0ConnectionAccessToken(connection, appConnectionDAL, kmsService); + const { audience } = connection.credentials; + await blockLocalAndPrivateIpAddresses(audience); + const clientSecret = generatePassword(); + + await request.request({ + method: "PATCH", + url: `${audience}clients/${clientId}`, + headers: { authorization: `Bearer ${accessToken}` }, + data: { + client_secret: clientSecret + } + }); + + return { clientId, clientSecret }; + }; + + const issueCredentials: TRotationFactoryIssueCredentials = async ( + callback + ) => { + const credentials = await $rotateClientSecret(); + + return callback(credentials); + }; + + const revokeCredentials: TRotationFactoryRevokeCredentials = async ( + _, + callback + ) => { + const accessToken = await getAuth0ConnectionAccessToken(connection, appConnectionDAL, kmsService); + const { audience } = connection.credentials; + await blockLocalAndPrivateIpAddresses(audience); + + // we just trigger an auth0 rotation to negate our credentials + await request.request({ + method: "POST", + url: `${audience}clients/${clientId}/rotate-secret`, + headers: { authorization: `Bearer ${accessToken}` } + }); + + return callback(); + }; + + const rotateCredentials: TRotationFactoryRotateCredentials = async ( + _, + callback + ) => { + const credentials = await $rotateClientSecret(); + + return callback(credentials); + }; + + const getSecretsPayload: TRotationFactoryGetSecretsPayload = ( + generatedCredentials + ) => { + const secrets = [ + { + key: secretsMapping.clientId, + value: generatedCredentials.clientId + }, + { + key: secretsMapping.clientSecret, + value: generatedCredentials.clientSecret + } + ]; + + return secrets; + }; + + return { + issueCredentials, + revokeCredentials, + rotateCredentials, + getSecretsPayload + }; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-schemas.ts new file mode 100644 index 000000000..3a0ba265b --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-schemas.ts @@ -0,0 +1,67 @@ +import { z } from "zod"; + +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + BaseCreateSecretRotationSchema, + BaseSecretRotationSchema, + BaseUpdateSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas"; +import { SecretRotations } from "@app/lib/api-docs"; +import { SecretNameSchema } from "@app/server/lib/schemas"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const Auth0ClientSecretRotationGeneratedCredentialsSchema = z + .object({ + clientId: z.string(), + clientSecret: z.string() + }) + .array() + .min(1) + .max(2); + +const Auth0ClientSecretRotationParametersSchema = z.object({ + clientId: z + .string() + .trim() + .min(1, "Client ID Required") + .describe(SecretRotations.PARAMETERS.AUTH0_CLIENT_SECRET.clientId) +}); + +const Auth0ClientSecretRotationSecretsMappingSchema = z.object({ + clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientId), + clientSecret: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientSecret) +}); + +export const Auth0ClientSecretRotationTemplateSchema = z.object({ + secretsMapping: z.object({ + clientId: z.string(), + clientSecret: z.string() + }) +}); + +export const Auth0ClientSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.Auth0ClientSecret).extend({ + type: z.literal(SecretRotation.Auth0ClientSecret), + parameters: Auth0ClientSecretRotationParametersSchema, + secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema +}); + +export const CreateAuth0ClientSecretRotationSchema = BaseCreateSecretRotationSchema( + SecretRotation.Auth0ClientSecret +).extend({ + parameters: Auth0ClientSecretRotationParametersSchema, + secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema +}); + +export const UpdateAuth0ClientSecretRotationSchema = BaseUpdateSecretRotationSchema( + SecretRotation.Auth0ClientSecret +).extend({ + parameters: Auth0ClientSecretRotationParametersSchema.optional(), + secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema.optional() +}); + +export const Auth0ClientSecretRotationListItemSchema = z.object({ + name: z.literal("Auth0 Client Secret"), + connection: z.literal(AppConnection.Auth0), + type: z.literal(SecretRotation.Auth0ClientSecret), + template: Auth0ClientSecretRotationTemplateSchema +}); diff --git a/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-types.ts new file mode 100644 index 000000000..b3bb4ec35 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-types.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +import { TAuth0Connection } from "@app/services/app-connection/auth0"; + +import { + Auth0ClientSecretRotationGeneratedCredentialsSchema, + Auth0ClientSecretRotationListItemSchema, + Auth0ClientSecretRotationSchema, + CreateAuth0ClientSecretRotationSchema +} from "./auth0-client-secret-rotation-schemas"; + +export type TAuth0ClientSecretRotation = z.infer; + +export type TAuth0ClientSecretRotationInput = z.infer; + +export type TAuth0ClientSecretRotationListItem = z.infer; + +export type TAuth0ClientSecretRotationWithConnection = TAuth0ClientSecretRotation & { + connection: TAuth0Connection; +}; + +export type TAuth0ClientSecretRotationGeneratedCredentials = z.infer< + typeof Auth0ClientSecretRotationGeneratedCredentialsSchema +>; diff --git a/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/index.ts b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/index.ts new file mode 100644 index 000000000..0c595be48 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/index.ts @@ -0,0 +1,3 @@ +export * from "./auth0-client-secret-rotation-constants"; +export * from "./auth0-client-secret-rotation-schemas"; +export * from "./auth0-client-secret-rotation-types"; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts index 178a12516..d43cacb3a 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts @@ -1,6 +1,7 @@ export enum SecretRotation { PostgresCredentials = "postgres-credentials", - MsSqlCredentials = "mssql-credentials" + MsSqlCredentials = "mssql-credentials", + Auth0ClientSecret = "auth0-client-secret" } export enum SecretRotationStatus { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts index 376b497f1..603b77cc1 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts @@ -3,6 +3,7 @@ import { AxiosError } from "axios"; import { getConfig } from "@app/lib/config/env"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret"; import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials"; import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials"; import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums"; @@ -16,7 +17,8 @@ import { const SECRET_ROTATION_LIST_OPTIONS: Record = { [SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION, - [SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION + [SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION, + [SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION }; export const listSecretRotationOptions = () => { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts index c0d59332b..1050c3419 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts @@ -3,10 +3,12 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums export const SECRET_ROTATION_NAME_MAP: Record = { [SecretRotation.PostgresCredentials]: "PostgreSQL Credentials", - [SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials" + [SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials", + [SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret" }; export const SECRET_ROTATION_CONNECTION_MAP: Record = { [SecretRotation.PostgresCredentials]: AppConnection.Postgres, - [SecretRotation.MsSqlCredentials]: AppConnection.MsSql + [SecretRotation.MsSqlCredentials]: AppConnection.MsSql, + [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0 }; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts index 1f55ac526..a828acb32 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts @@ -13,6 +13,7 @@ import { ProjectPermissionSecretRotationActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns"; import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; import { calculateNextRotationAt, @@ -41,6 +42,7 @@ import { TRotationFactory, TSecretRotationRotateGeneratedCredentials, TSecretRotationV2, + TSecretRotationV2GeneratedCredentials, TSecretRotationV2Raw, TSecretRotationV2WithConnection, TUpdateSecretRotationV2DTO @@ -53,6 +55,7 @@ import { DatabaseErrorCode } from "@app/lib/error-codes"; import { BadRequestError, DatabaseError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; import { QueueJobs, TQueueServiceFactory } from "@app/queue"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { ActorType } from "@app/services/auth/auth-type"; @@ -97,15 +100,21 @@ export type TSecretRotationV2ServiceFactoryDep = { secretQueueService: Pick; snapshotService: Pick; queueService: Pick; + appConnectionDAL: Pick; }; export type TSecretRotationV2ServiceFactory = ReturnType; const MAX_GENERATED_CREDENTIALS_LENGTH = 2; -const SECRET_ROTATION_FACTORY_MAP: Record = { - [SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory, - [SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory +type TRotationFactoryImplementation = TRotationFactory< + TSecretRotationV2WithConnection, + TSecretRotationV2GeneratedCredentials +>; +const SECRET_ROTATION_FACTORY_MAP: Record = { + [SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, + [SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, + [SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation }; export const secretRotationV2ServiceFactory = ({ @@ -125,7 +134,8 @@ export const secretRotationV2ServiceFactory = ({ secretQueueService, snapshotService, keyStore, - queueService + queueService, + appConnectionDAL }: TSecretRotationV2ServiceFactoryDep) => { const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => { const appCfg = getConfig(); @@ -429,11 +439,15 @@ export const secretRotationV2ServiceFactory = ({ // validates permission to connect and app is valid for rotation type const connection = await appConnectionService.connectAppConnectionById(typeApp, payload.connectionId, actor); - const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type]({ - parameters: payload.parameters, - secretsMapping, - connection - } as TSecretRotationV2WithConnection); + const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type]( + { + parameters: payload.parameters, + secretsMapping, + connection + } as TSecretRotationV2WithConnection, + appConnectionDAL, + kmsService + ); try { const currentTime = new Date(); @@ -441,7 +455,7 @@ export const secretRotationV2ServiceFactory = ({ // callback structure to support transactional rollback when possible const secretRotation = await rotationFactory.issueCredentials(async (newCredentials) => { const encryptedGeneratedCredentials = await encryptSecretRotationCredentials({ - generatedCredentials: [newCredentials], + generatedCredentials: [newCredentials] as TSecretRotationV2GeneratedCredentials, projectId, kmsService }); @@ -740,32 +754,37 @@ export const secretRotationV2ServiceFactory = ({ message: `Secret Rotation with ID "${rotationId}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}` }); - const deleteTransaction = secretRotationV2DAL.transaction(async (tx) => { - if (deleteSecrets) { - await fnSecretBulkDelete({ - secretDAL: secretV2BridgeDAL, - secretQueueService, - inputSecrets: Object.values(secretsMapping as TSecretRotationV2["secretsMapping"]).map((secretKey) => ({ - secretKey, - type: SecretType.Shared - })), - projectId, - folderId, - actorId: actor.id, // not actually used since rotated secrets are shared - tx - }); - } + const deleteTransaction = async () => + secretRotationV2DAL.transaction(async (tx) => { + if (deleteSecrets) { + await fnSecretBulkDelete({ + secretDAL: secretV2BridgeDAL, + secretQueueService, + inputSecrets: Object.values(secretsMapping as TSecretRotationV2["secretsMapping"]).map((secretKey) => ({ + secretKey, + type: SecretType.Shared + })), + projectId, + folderId, + actorId: actor.id, // not actually used since rotated secrets are shared + tx + }); + } - return secretRotationV2DAL.deleteById(rotationId, tx); - }); + return secretRotationV2DAL.deleteById(rotationId, tx); + }); if (revokeGeneratedCredentials) { const appConnection = await decryptAppConnection(connection, kmsService); - const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type]({ - ...secretRotation, - connection: appConnection - } as TSecretRotationV2WithConnection); + const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type]( + { + ...secretRotation, + connection: appConnection + } as TSecretRotationV2WithConnection, + appConnectionDAL, + kmsService + ); const generatedCredentials = await decryptSecretRotationCredentials({ encryptedGeneratedCredentials, @@ -773,9 +792,9 @@ export const secretRotationV2ServiceFactory = ({ kmsService }); - await rotationFactory.revokeCredentials(generatedCredentials, async () => deleteTransaction); + await rotationFactory.revokeCredentials(generatedCredentials, deleteTransaction); } else { - await deleteTransaction; + await deleteTransaction(); } if (deleteSecrets) { @@ -840,10 +859,14 @@ export const secretRotationV2ServiceFactory = ({ const inactiveCredentials = generatedCredentials[inactiveIndex]; - const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation]({ - ...secretRotation, - connection: appConnection - } as TSecretRotationV2WithConnection); + const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation]( + { + ...secretRotation, + connection: appConnection + } as TSecretRotationV2WithConnection, + appConnectionDAL, + kmsService + ); const updatedRotation = await rotationFactory.rotateCredentials(inactiveCredentials, async (newCredentials) => { const updatedCredentials = [...generatedCredentials]; @@ -851,7 +874,7 @@ export const secretRotationV2ServiceFactory = ({ const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({ projectId, - generatedCredentials: updatedCredentials, + generatedCredentials: updatedCredentials as TSecretRotationV2GeneratedCredentials, kmsService }); diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts index 7102f7de3..c52fa5465 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts @@ -1,8 +1,17 @@ import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types"; import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types"; import { OrderByDirection } from "@app/lib/types"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; +import { + TAuth0ClientSecretRotation, + TAuth0ClientSecretRotationGeneratedCredentials, + TAuth0ClientSecretRotationInput, + TAuth0ClientSecretRotationListItem, + TAuth0ClientSecretRotationWithConnection +} from "./auth0-client-secret"; import { TMsSqlCredentialsRotation, TMsSqlCredentialsRotationInput, @@ -18,17 +27,26 @@ import { import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal"; import { SecretRotation } from "./secret-rotation-v2-enums"; -export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation; +export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation; export type TSecretRotationV2WithConnection = | TPostgresCredentialsRotationWithConnection - | TMsSqlCredentialsRotationWithConnection; + | TMsSqlCredentialsRotationWithConnection + | TAuth0ClientSecretRotationWithConnection; -export type TSecretRotationV2GeneratedCredentials = TSqlCredentialsRotationGeneratedCredentials; +export type TSecretRotationV2GeneratedCredentials = + | TSqlCredentialsRotationGeneratedCredentials + | TAuth0ClientSecretRotationGeneratedCredentials; -export type TSecretRotationV2Input = TPostgresCredentialsRotationInput | TMsSqlCredentialsRotationInput; +export type TSecretRotationV2Input = + | TPostgresCredentialsRotationInput + | TMsSqlCredentialsRotationInput + | TAuth0ClientSecretRotationInput; -export type TSecretRotationV2ListItem = TPostgresCredentialsRotationListItem | TMsSqlCredentialsRotationListItem; +export type TSecretRotationV2ListItem = + | TPostgresCredentialsRotationListItem + | TMsSqlCredentialsRotationListItem + | TAuth0ClientSecretRotationListItem; export type TSecretRotationV2Raw = NonNullable>>; @@ -129,27 +147,34 @@ export type TSecretRotationSendNotificationJobPayload = { // transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the // third party credential changes (when supported), preventing credentials getting out of sync -export type TRotationFactoryIssueCredentials = ( - callback: (newCredentials: TSecretRotationV2GeneratedCredentials[number]) => Promise +export type TRotationFactoryIssueCredentials = ( + callback: (newCredentials: T[number]) => Promise ) => Promise; -export type TRotationFactoryRevokeCredentials = ( - generatedCredentials: TSecretRotationV2GeneratedCredentials, +export type TRotationFactoryRevokeCredentials = ( + generatedCredentials: T, callback: () => Promise ) => Promise; -export type TRotationFactoryRotateCredentials = ( - credentialsToRevoke: TSecretRotationV2GeneratedCredentials[number] | undefined, - callback: (newCredentials: TSecretRotationV2GeneratedCredentials[number]) => Promise +export type TRotationFactoryRotateCredentials = ( + credentialsToRevoke: T[number] | undefined, + callback: (newCredentials: T[number]) => Promise ) => Promise; -export type TRotationFactoryGetSecretsPayload = ( - generatedCredentials: TSecretRotationV2GeneratedCredentials[number] +export type TRotationFactoryGetSecretsPayload = ( + generatedCredentials: T[number] ) => { key: string; value: string }[]; -export type TRotationFactory = (secretRotation: TSecretRotationV2WithConnection) => { - issueCredentials: TRotationFactoryIssueCredentials; - revokeCredentials: TRotationFactoryRevokeCredentials; - rotateCredentials: TRotationFactoryRotateCredentials; - getSecretsPayload: TRotationFactoryGetSecretsPayload; +export type TRotationFactory< + T extends TSecretRotationV2WithConnection, + C extends TSecretRotationV2GeneratedCredentials +> = ( + secretRotation: T, + appConnectionDAL: Pick, + kmsService: Pick +) => { + issueCredentials: TRotationFactoryIssueCredentials; + revokeCredentials: TRotationFactoryRevokeCredentials; + rotateCredentials: TRotationFactoryRotateCredentials; + getSecretsPayload: TRotationFactoryGetSecretsPayload; }; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts index 0c4bbd014..2db9c0251 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts @@ -1,9 +1,11 @@ import { z } from "zod"; +import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret"; import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; export const SecretRotationV2Schema = z.discriminatedUnion("type", [ PostgresCredentialsRotationSchema, - MsSqlCredentialsRotationSchema + MsSqlCredentialsRotationSchema, + Auth0ClientSecretRotationSchema ]); diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts index 17983eb43..12e9b5964 100644 --- a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts @@ -1,6 +1,5 @@ -import { randomInt } from "crypto"; - import { + TRotationFactory, TRotationFactoryGetSecretsPayload, TRotationFactoryIssueCredentials, TRotationFactoryRevokeCredentials, @@ -8,94 +7,12 @@ import { } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql"; +import { generatePassword } from "../utils"; import { TSqlCredentialsRotationGeneratedCredentials, TSqlCredentialsRotationWithConnection } from "./sql-credentials-rotation-types"; -const DEFAULT_PASSWORD_REQUIREMENTS = { - length: 48, - required: { - lowercase: 1, - uppercase: 1, - digits: 1, - symbols: 0 - }, - allowedSymbols: "-_.~!*" -}; - -const generatePassword = () => { - try { - const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS; - - const chars = { - lowercase: "abcdefghijklmnopqrstuvwxyz", - uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", - digits: "0123456789", - symbols: allowedSymbols || "-_.~!*" - }; - - const parts: string[] = []; - - if (required.lowercase > 0) { - parts.push( - ...Array(required.lowercase) - .fill(0) - .map(() => chars.lowercase[randomInt(chars.lowercase.length)]) - ); - } - - if (required.uppercase > 0) { - parts.push( - ...Array(required.uppercase) - .fill(0) - .map(() => chars.uppercase[randomInt(chars.uppercase.length)]) - ); - } - - if (required.digits > 0) { - parts.push( - ...Array(required.digits) - .fill(0) - .map(() => chars.digits[randomInt(chars.digits.length)]) - ); - } - - if (required.symbols > 0) { - parts.push( - ...Array(required.symbols) - .fill(0) - .map(() => chars.symbols[randomInt(chars.symbols.length)]) - ); - } - - const requiredTotal = Object.values(required).reduce((a, b) => a + b, 0); - const remainingLength = Math.max(length - requiredTotal, 0); - - const allowedChars = Object.entries(chars) - .filter(([key]) => required[key as keyof typeof required] > 0) - .map(([, value]) => value) - .join(""); - - parts.push( - ...Array(remainingLength) - .fill(0) - .map(() => allowedChars[randomInt(allowedChars.length)]) - ); - - // shuffle the array to mix up the characters - for (let i = parts.length - 1; i > 0; i -= 1) { - const j = randomInt(i + 1); - [parts[i], parts[j]] = [parts[j], parts[i]]; - } - - return parts.join(""); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : "Unknown error"; - throw new Error(`Failed to generate password: ${message}`); - } -}; - const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGeneratedCredentials) => { const error = e as Error; @@ -110,7 +27,10 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat return redactedMessage; }; -export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRotationWithConnection) => { +export const sqlCredentialsRotationFactory: TRotationFactory< + TSqlCredentialsRotationWithConnection, + TSqlCredentialsRotationGeneratedCredentials +> = (secretRotation) => { const { connection, parameters: { username1, username2 }, @@ -118,7 +38,7 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot secretsMapping } = secretRotation; - const validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => { + const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => { const client = await getSqlConnectionClient({ ...connection, credentials: { @@ -136,7 +56,9 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot } }; - const issueCredentials: TRotationFactoryIssueCredentials = async (callback) => { + const issueCredentials: TRotationFactoryIssueCredentials = async ( + callback + ) => { const client = await getSqlConnectionClient(connection); // For SQL, since we get existing users, we change both their passwords @@ -159,13 +81,16 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot } for await (const credentials of credentialsSet) { - await validateCredentials(credentials); + await $validateCredentials(credentials); } return callback(credentialsSet[0]); }; - const revokeCredentials: TRotationFactoryRevokeCredentials = async (credentialsToRevoke, callback) => { + const revokeCredentials: TRotationFactoryRevokeCredentials = async ( + credentialsToRevoke, + callback + ) => { const client = await getSqlConnectionClient(connection); const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() })); @@ -186,7 +111,10 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot return callback(); }; - const rotateCredentials: TRotationFactoryRotateCredentials = async (_, callback) => { + const rotateCredentials: TRotationFactoryRotateCredentials = async ( + _, + callback + ) => { const client = await getSqlConnectionClient(connection); // generate new password for the next active user @@ -200,12 +128,14 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot await client.destroy(); } - await validateCredentials(credentials); + await $validateCredentials(credentials); return callback(credentials); }; - const getSecretsPayload: TRotationFactoryGetSecretsPayload = (generatedCredentials) => { + const getSecretsPayload: TRotationFactoryGetSecretsPayload = ( + generatedCredentials + ) => { const { username, password } = secretsMapping; const secrets = [ @@ -226,7 +156,6 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot issueCredentials, revokeCredentials, rotateCredentials, - getSecretsPayload, - validateCredentials + getSecretsPayload }; }; diff --git a/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts new file mode 100644 index 000000000..dfe4c22ed --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts @@ -0,0 +1,84 @@ +import { randomInt } from "crypto"; + +const DEFAULT_PASSWORD_REQUIREMENTS = { + length: 48, + required: { + lowercase: 1, + uppercase: 1, + digits: 1, + symbols: 0 + }, + allowedSymbols: "-_.~!*" +}; + +export const generatePassword = () => { + try { + const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS; + + const chars = { + lowercase: "abcdefghijklmnopqrstuvwxyz", + uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + digits: "0123456789", + symbols: allowedSymbols || "-_.~!*" + }; + + const parts: string[] = []; + + if (required.lowercase > 0) { + parts.push( + ...Array(required.lowercase) + .fill(0) + .map(() => chars.lowercase[randomInt(chars.lowercase.length)]) + ); + } + + if (required.uppercase > 0) { + parts.push( + ...Array(required.uppercase) + .fill(0) + .map(() => chars.uppercase[randomInt(chars.uppercase.length)]) + ); + } + + if (required.digits > 0) { + parts.push( + ...Array(required.digits) + .fill(0) + .map(() => chars.digits[randomInt(chars.digits.length)]) + ); + } + + if (required.symbols > 0) { + parts.push( + ...Array(required.symbols) + .fill(0) + .map(() => chars.symbols[randomInt(chars.symbols.length)]) + ); + } + + const requiredTotal = Object.values(required).reduce((a, b) => a + b, 0); + const remainingLength = Math.max(length - requiredTotal, 0); + + const allowedChars = Object.entries(chars) + .filter(([key]) => required[key as keyof typeof required] > 0) + .map(([, value]) => value) + .join(""); + + parts.push( + ...Array(remainingLength) + .fill(0) + .map(() => allowedChars[randomInt(allowedChars.length)]) + ); + + // shuffle the array to mix up the characters + for (let i = parts.length - 1; i > 0; i -= 1) { + const j = randomInt(i + 1); + [parts[i], parts[j]] = [parts[j], parts[i]]; + } + + return parts.join(""); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : "Unknown error"; + throw new Error(`Failed to generate password: ${message}`); + } +}; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index c2cae374a..ff07940bc 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -478,7 +478,8 @@ export const PROJECTS = { name: "The new name of the project.", projectDescription: "An optional description label for the project.", autoCapitalization: "Disable or enable auto-capitalization for the project.", - slug: "An optional slug for the project. (must be unique within the organization)" + slug: "An optional slug for the project. (must be unique within the organization)", + hasDeleteProtection: "Enable or disable delete protection for the project." }, GET_KEY: { workspaceId: "The ID of the project to get the key from." @@ -1782,6 +1783,12 @@ export const AppConnections = { connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to be deleted.` }), CREDENTIALS: { + AUTH0_CONNECTION: { + domain: "The domain of the Auth0 instance to connect to.", + clientId: "Your Auth0 application's Client ID.", + clientSecret: "Your Auth0 application's Client Secret.", + audience: "The unique identifier of the target API you want to access." + }, SQL_CONNECTION: { host: "The hostname of the database server.", port: "The port number of the database.", @@ -2005,12 +2012,19 @@ export const SecretRotations = { "The username of the first login to rotate passwords for. This user must already exists in your database.", username2: "The username of the second login to rotate passwords for. This user must already exists in your database." + }, + AUTH0_CLIENT_SECRET: { + clientId: "The client ID of the Auth0 Application to rotate the client secret for." } }, SECRETS_MAPPING: { SQL_CREDENTIALS: { username: "The name of the secret that the active username will be mapped to.", password: "The name of the secret that the generated password will be mapped to." + }, + AUTH0_CLIENT_SECRET: { + clientId: "The name of the secret that the client ID will be mapped to.", + clientSecret: "The name of the secret that the rotated client secret will be mapped to." } } }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 9d8585361..c9f2811b5 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1255,7 +1255,8 @@ export const registerRoutes = async ( userDAL, permissionService, projectDAL, - accessTokenQueue + accessTokenQueue, + smtpService }); const identityService = identityServiceFactory({ @@ -1416,7 +1417,8 @@ export const registerRoutes = async ( identityAccessTokenDAL, secretSharingDAL, secretVersionV2DAL: secretVersionV2BridgeDAL, - identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL + identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL, + serviceTokenService }); const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({ @@ -1549,7 +1551,8 @@ export const registerRoutes = async ( resourceMetadataDAL, snapshotService, secretQueueService, - queueService + queueService, + appConnectionDAL }); await secretRotationV2QueueServiceFactory({ diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 2a87cf7cf..da300981c 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -260,7 +260,8 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ upgradeStatus: true, pitVersionLimit: true, kmsCertificateKeyId: true, - auditLogsRetentionDays: true + auditLogsRetentionDays: true, + hasDeleteProtection: true }); export const SanitizedTagSchema = SecretTagsSchema.pick({ diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 3b73a72e4..c8d2a976d 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { Auth0ConnectionListItemSchema, SanitizedAuth0ConnectionSchema } from "@app/services/app-connection/auth0"; import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws"; import { AzureAppConfigurationConnectionListItemSchema, @@ -56,7 +57,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedPostgresConnectionSchema.options, ...SanitizedMsSqlConnectionSchema.options, ...SanitizedCamundaConnectionSchema.options, - ...SanitizedWindmillConnectionSchema.options + ...SanitizedWindmillConnectionSchema.options, + ...SanitizedAuth0ConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -72,7 +74,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ PostgresConnectionListItemSchema, MsSqlConnectionListItemSchema, CamundaConnectionListItemSchema, - WindmillConnectionListItemSchema + WindmillConnectionListItemSchema, + Auth0ConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/auth0-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/auth0-connection-router.ts new file mode 100644 index 000000000..db17c8eac --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/auth0-connection-router.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateAuth0ConnectionSchema, + SanitizedAuth0ConnectionSchema, + UpdateAuth0ConnectionSchema +} from "@app/services/app-connection/auth0"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerAuth0ConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Auth0, + server, + sanitizedResponseSchema: SanitizedAuth0ConnectionSchema, + createSchema: CreateAuth0ConnectionSchema, + updateSchema: UpdateAuth0ConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + + server.route({ + method: "GET", + url: `/:connectionId/clients`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + clients: z.object({ name: z.string(), id: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const clients = await server.services.appConnection.auth0.listClients(connectionId, req.permission); + + return { clients }; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index ec2b79060..a833b6882 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -1,3 +1,4 @@ +import { registerAuth0ConnectionRouter } from "@app/server/routes/v1/app-connection-routers/auth0-connection-router"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { registerAwsConnectionRouter } from "./aws-connection-router"; @@ -30,5 +31,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { 200: z.object({ organizations: sanitizedOrganizationSchema .extend({ - orgAuthMethod: z.string() + orgAuthMethod: z.string(), + userRole: z.string() }) .array() }) @@ -259,7 +260,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(), enforceMfa: z.boolean().optional(), selectedMfaMethod: z.nativeEnum(MfaMethod).optional(), - allowSecretSharingOutsideOrganization: z.boolean().optional() + allowSecretSharingOutsideOrganization: z.boolean().optional(), + bypassOrgAuthEnabled: z.boolean().optional() }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 2496d8f62..4182ce389 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -312,6 +312,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { .optional() .describe(PROJECTS.UPDATE.projectDescription), autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization), + hasDeleteProtection: z.boolean().optional().describe(PROJECTS.UPDATE.hasDeleteProtection), slug: z .string() .trim() @@ -340,6 +341,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { name: req.body.name, description: req.body.description, autoCapitalization: req.body.autoCapitalization, + hasDeleteProtection: req.body.hasDeleteProtection, slug: req.body.slug }, actorAuthMethod: req.permission.authMethod, @@ -390,6 +392,43 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "POST", + url: "/:workspaceId/delete-protection", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + hasDeleteProtection: z.boolean() + }), + response: { + 200: z.object({ + message: z.string(), + workspace: SanitizedProjectSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const workspace = await server.services.project.toggleDeleteProtection({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + hasDeleteProtection: req.body.hasDeleteProtection + }); + return { + message: "Successfully changed workspace settings", + workspace + }; + } + }); + server.route({ method: "PUT", url: "/:workspaceSlug/version-limit", diff --git a/backend/src/server/routes/v1/secret-folder-router.ts b/backend/src/server/routes/v1/secret-folder-router.ts index b55564d80..dbfa715ea 100644 --- a/backend/src/server/routes/v1/secret-folder-router.ts +++ b/backend/src/server/routes/v1/secret-folder-router.ts @@ -39,17 +39,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .string() .trim() .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.CREATE.path), + .describe(FOLDERS.CREATE.path) + .optional(), // backward compatiability with cli directory: z .string() .trim() .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if directory is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.CREATE.directory), + .describe(FOLDERS.CREATE.directory) + .optional(), description: z.string().optional().nullable().describe(FOLDERS.CREATE.description) }), response: { @@ -60,7 +62,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const path = req.body.path || req.body.directory; + const path = req.body.path || req.body.directory || "/"; const folder = await server.services.folder.createFolder({ actorId: req.permission.id, actor: req.permission.type, @@ -120,17 +122,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .string() .trim() .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.UPDATE.path), + .describe(FOLDERS.UPDATE.path) + .optional(), // backward compatiability with cli directory: z .string() .trim() .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if directory is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.UPDATE.directory), + .describe(FOLDERS.UPDATE.directory) + .optional(), description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description) }), response: { @@ -141,7 +145,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const path = req.body.path || req.body.directory; + const path = req.body.path || req.body.directory || "/"; const { folder, old } = await server.services.folder.updateFolder({ actorId: req.permission.id, actor: req.permission.type, @@ -271,17 +275,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .string() .trim() .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.DELETE.path), + .describe(FOLDERS.DELETE.path) + .optional(), // keep this here as cli need directory directory: z .string() .trim() .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if directory is undefined .transform(removeTrailingSlash) .describe(FOLDERS.DELETE.directory) + .optional() }), response: { 200: z.object({ @@ -291,7 +297,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const path = req.body.path || req.body.directory; + const path = req.body.path || req.body.directory || "/"; const folder = await server.services.folder.deleteFolder({ actorId: req.permission.id, actor: req.permission.type, @@ -339,18 +345,18 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => path: z .string() .trim() - .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.LIST.path), + .describe(FOLDERS.LIST.path) + .optional(), // backward compatiability with cli directory: z .string() .trim() - .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if directory is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.LIST.directory), + .describe(FOLDERS.LIST.directory) + .optional(), recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive) }), response: { @@ -363,7 +369,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const path = req.query.path || req.query.directory; + const path = req.query.path || req.query.directory || "/"; const folders = await server.services.folder.getFolders({ actorId: req.permission.id, actor: req.permission.type, diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 6e4a8170e..c8f68a926 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -303,7 +303,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { body: z.object({ name: z.string().trim().optional().describe(PROJECTS.UPDATE.name), description: z.string().trim().optional().describe(PROJECTS.UPDATE.projectDescription), - autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization) + autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization), + hasDeleteProtection: z.boolean().optional().describe(PROJECTS.UPDATE.hasDeleteProtection) }), response: { 200: SanitizedProjectSchema @@ -321,7 +322,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { update: { name: req.body.name, description: req.body.description, - autoCapitalization: req.body.autoCapitalization + autoCapitalization: req.body.autoCapitalization, + hasDeleteProtection: req.body.hasDeleteProtection }, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index ca5edf440..6b6048f2a 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -11,7 +11,8 @@ export enum AppConnection { Postgres = "postgres", MsSql = "mssql", Camunda = "camunda", - Windmill = "windmill" + Windmill = "windmill", + Auth0 = "auth0" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index b8eebf6bc..7e08a92b4 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -16,6 +16,7 @@ import { TAppConnectionCredentialsValidator, TAppConnectionTransitionCredentialsToPlatform } from "./app-connection-types"; +import { Auth0ConnectionMethod, getAuth0ConnectionListItem, validateAuth0ConnectionCredentials } from "./auth0"; import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws"; import { AzureAppConfigurationConnectionMethod, @@ -69,7 +70,8 @@ export const listAppConnectionOptions = () => { getPostgresConnectionListItem(), getMsSqlConnectionListItem(), getCamundaConnectionListItem(), - getWindmillConnectionListItem() + getWindmillConnectionListItem(), + getAuth0ConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -115,26 +117,29 @@ export const decryptAppConnectionCredentials = async ({ return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"]; }; -const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record = { - [AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.AzureAppConfiguration]: - validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator -}; - export const validateAppConnectionCredentials = async ( appConnection: TAppConnectionConfig -): Promise => VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); +): Promise => { + const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record = { + [AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.AzureAppConfiguration]: + validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator + }; + + return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); +}; export const getAppConnectionMethodName = (method: TAppConnection["method"]) => { switch (method) { @@ -163,6 +168,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => return "Username & Password"; case WindmillConnectionMethod.AccessToken: return "Access Token"; + case Auth0ConnectionMethod.ClientCredentials: + return "Client Credentials"; default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled App Connection Method: ${method}`); @@ -206,5 +213,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported, [AppConnection.Camunda]: platformManagedCredentialsNotSupported, [AppConnection.Vercel]: platformManagedCredentialsNotSupported, - [AppConnection.Windmill]: platformManagedCredentialsNotSupported + [AppConnection.Windmill]: platformManagedCredentialsNotSupported, + [AppConnection.Auth0]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 15cc78392..762a9bcf2 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -13,5 +13,6 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Postgres]: "PostgreSQL", [AppConnection.MsSql]: "Microsoft SQL Server", [AppConnection.Camunda]: "Camunda", - [AppConnection.Windmill]: "Windmill" + [AppConnection.Windmill]: "Windmill", + [AppConnection.Auth0]: "Auth0" }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 608da5607..5293761a8 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -14,6 +14,7 @@ import { TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM, validateAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; +import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TAppConnectionDALFactory } from "./app-connection-dal"; @@ -27,6 +28,7 @@ import { TUpdateAppConnectionDTO, TValidateAppConnectionCredentialsSchema } from "./app-connection-types"; +import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0"; import { ValidateAwsConnectionCredentialsSchema } from "./aws"; import { awsConnectionService } from "./aws/aws-connection-service"; import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration"; @@ -71,7 +73,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record>>; @@ -110,6 +117,7 @@ export type TAppConnectionInput = { id: string } & ( | TMsSqlConnectionInput | TCamundaConnectionInput | TWindmillConnectionInput + | TAuth0ConnectionInput ); export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; @@ -135,7 +143,8 @@ export type TAppConnectionConfig = | TVercelConnectionConfig | TSqlConnectionConfig | TCamundaConnectionConfig - | TWindmillConnectionConfig; + | TWindmillConnectionConfig + | TAuth0ConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -150,7 +159,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateCamundaConnectionCredentialsSchema | TValidateTerraformCloudConnectionCredentialsSchema | TValidateVercelConnectionCredentialsSchema - | TValidateWindmillConnectionCredentialsSchema; + | TValidateWindmillConnectionCredentialsSchema + | TValidateAuth0ConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/auth0/auth0-connection-enums.ts b/backend/src/services/app-connection/auth0/auth0-connection-enums.ts new file mode 100644 index 000000000..07d725bea --- /dev/null +++ b/backend/src/services/app-connection/auth0/auth0-connection-enums.ts @@ -0,0 +1,3 @@ +export enum Auth0ConnectionMethod { + ClientCredentials = "client-credentials" +} diff --git a/backend/src/services/app-connection/auth0/auth0-connection-fns.ts b/backend/src/services/app-connection/auth0/auth0-connection-fns.ts new file mode 100644 index 000000000..5a9989b43 --- /dev/null +++ b/backend/src/services/app-connection/auth0/auth0-connection-fns.ts @@ -0,0 +1,97 @@ +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { Auth0ConnectionMethod } from "./auth0-connection-enums"; +import { TAuth0AccessTokenResponse, TAuth0Connection, TAuth0ConnectionConfig } from "./auth0-connection-types"; + +export const getAuth0ConnectionListItem = () => { + return { + name: "Auth0" as const, + app: AppConnection.Auth0 as const, + methods: Object.values(Auth0ConnectionMethod) as [Auth0ConnectionMethod.ClientCredentials] + }; +}; + +const authorizeAuth0Connection = async ({ + clientId, + clientSecret, + domain, + audience +}: TAuth0ConnectionConfig["credentials"]) => { + const instanceUrl = domain.startsWith("http") ? domain : `https://${domain}`; + await blockLocalAndPrivateIpAddresses(instanceUrl); + + const { data } = await request.request({ + method: "POST", + url: `${removeTrailingSlash(instanceUrl)}/oauth/token`, + headers: { "content-type": "application/x-www-form-urlencoded" }, + data: new URLSearchParams({ + grant_type: "client_credentials", // this will need to be resolved if we support methods other than client credentials + client_id: clientId, + client_secret: clientSecret, + audience + }) + }); + + if (data.token_type !== "Bearer") { + throw new Error(`Unhandled token type: ${data.token_type}`); + } + + return { + accessToken: data.access_token, + // cap token lifespan to 10 minutes + expiresAt: Math.min(data.expires_in * 1000, 600000) + Date.now() + }; +}; + +export const getAuth0ConnectionAccessToken = async ( + { id, orgId, credentials }: TAuth0Connection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const { expiresAt, accessToken } = credentials; + + // get new token if expired or less than 5 minutes until expiry + if (Date.now() < expiresAt - 300000) { + return accessToken; + } + + const authData = await authorizeAuth0Connection(credentials); + + const updatedCredentials: TAuth0Connection["credentials"] = { + ...credentials, + ...authData + }; + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: updatedCredentials, + orgId, + kmsService + }); + + await appConnectionDAL.updateById(id, { encryptedCredentials }); + + return authData.accessToken; +}; + +export const validateAuth0ConnectionCredentials = async ({ credentials }: TAuth0ConnectionConfig) => { + try { + const { accessToken, expiresAt } = await authorizeAuth0Connection(credentials); + + return { + ...credentials, + accessToken, + expiresAt + }; + } catch (e: unknown) { + throw new BadRequestError({ + message: (e as Error).message ?? `Unable to validate connection: verify credentials` + }); + } +}; diff --git a/backend/src/services/app-connection/auth0/auth0-connection-schemas.ts b/backend/src/services/app-connection/auth0/auth0-connection-schemas.ts new file mode 100644 index 000000000..67992a503 --- /dev/null +++ b/backend/src/services/app-connection/auth0/auth0-connection-schemas.ts @@ -0,0 +1,94 @@ +import { z } from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { Auth0ConnectionMethod } from "./auth0-connection-enums"; + +export const Auth0ConnectionClientCredentialsInputCredentialsSchema = z.object({ + domain: z.string().trim().min(1, "Domain required").describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.domain), + clientId: z + .string() + .trim() + .min(1, "Client ID required") + .describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.clientId), + clientSecret: z + .string() + .trim() + .min(1, "Client Secret required") + .describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.clientSecret), + audience: z + .string() + .trim() + .url() + .min(1, "Audience required") + .describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.audience) +}); + +const Auth0ConnectionClientCredentialsOutputCredentialsSchema = z + .object({ + accessToken: z.string(), + expiresAt: z.number() + }) + .merge(Auth0ConnectionClientCredentialsInputCredentialsSchema); + +const BaseAuth0ConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.Auth0) +}); + +export const Auth0ConnectionSchema = z.intersection( + BaseAuth0ConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(Auth0ConnectionMethod.ClientCredentials), + credentials: Auth0ConnectionClientCredentialsOutputCredentialsSchema + }) + ]) +); + +export const SanitizedAuth0ConnectionSchema = z.discriminatedUnion("method", [ + BaseAuth0ConnectionSchema.extend({ + method: z.literal(Auth0ConnectionMethod.ClientCredentials), + credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.pick({ + domain: true, + clientId: true, + audience: true + }) + }) +]); + +export const ValidateAuth0ConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(Auth0ConnectionMethod.ClientCredentials) + .describe(AppConnections.CREATE(AppConnection.Auth0).method), + credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Auth0).credentials + ) + }) +]); + +export const CreateAuth0ConnectionSchema = ValidateAuth0ConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Auth0) +); + +export const UpdateAuth0ConnectionSchema = z + .object({ + credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Auth0).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Auth0)); + +export const Auth0ConnectionListItemSchema = z.object({ + name: z.literal("Auth0"), + app: z.literal(AppConnection.Auth0), + // the below is preferable but currently breaks with our zod to json schema parser + // methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]), + methods: z.nativeEnum(Auth0ConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/auth0/auth0-connection-service.ts b/backend/src/services/app-connection/auth0/auth0-connection-service.ts new file mode 100644 index 000000000..693c55ea6 --- /dev/null +++ b/backend/src/services/app-connection/auth0/auth0-connection-service.ts @@ -0,0 +1,71 @@ +import { request } from "@app/lib/config/request"; +import { OrgServiceActor } from "@app/lib/types"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { getAuth0ConnectionAccessToken } from "@app/services/app-connection/auth0/auth0-connection-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { TAuth0Connection, TAuth0ListClient, TAuth0ListClientsResponse } from "./auth0-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +const listAuth0Clients = async ( + appConnection: TAuth0Connection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const accessToken = await getAuth0ConnectionAccessToken(appConnection, appConnectionDAL, kmsService); + + const { audience, clientId: connectionClientId } = appConnection.credentials; + await blockLocalAndPrivateIpAddresses(audience); + + const clients: TAuth0ListClient[] = []; + let hasMore = true; + let page = 0; + + while (hasMore) { + // eslint-disable-next-line no-await-in-loop + const { data: clientsPage } = await request.get(`${audience}clients`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + }, + params: { + include_totals: true, + per_page: 100, + page + } + }); + + clients.push(...clientsPage.clients); + page += 1; + hasMore = clientsPage.total > clients.length; + } + + return ( + clients.filter((client) => client.client_id !== connectionClientId && client.name !== "All Applications") ?? [] + ); +}; + +export const auth0ConnectionService = ( + getAppConnection: TGetAppConnectionFunc, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const listClients = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Auth0, connectionId, actor); + + const clients = await listAuth0Clients(appConnection, appConnectionDAL, kmsService); + + return clients.map((client) => ({ id: client.client_id, name: client.name })); + }; + + return { + listClients + }; +}; diff --git a/backend/src/services/app-connection/auth0/auth0-connection-types.ts b/backend/src/services/app-connection/auth0/auth0-connection-types.ts new file mode 100644 index 000000000..ebb601946 --- /dev/null +++ b/backend/src/services/app-connection/auth0/auth0-connection-types.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { + Auth0ConnectionSchema, + CreateAuth0ConnectionSchema, + ValidateAuth0ConnectionCredentialsSchema +} from "./auth0-connection-schemas"; + +export type TAuth0Connection = z.infer; + +export type TAuth0ConnectionInput = z.infer & { + app: AppConnection.Auth0; +}; + +export type TValidateAuth0ConnectionCredentialsSchema = typeof ValidateAuth0ConnectionCredentialsSchema; + +export type TAuth0ConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type TAuth0AccessTokenResponse = { + access_token: string; + expires_in: number; + scope: string; + token_type: string; +}; + +export type TAuth0ListClient = { + name: string; + client_id: string; +}; + +export type TAuth0ListClientsResponse = { + total: number; + clients: TAuth0ListClient[]; +}; diff --git a/backend/src/services/app-connection/auth0/index.ts b/backend/src/services/app-connection/auth0/index.ts new file mode 100644 index 000000000..310ae3ea8 --- /dev/null +++ b/backend/src/services/app-connection/auth0/index.ts @@ -0,0 +1,4 @@ +export * from "./auth0-connection-enums"; +export * from "./auth0-connection-fns"; +export * from "./auth0-connection-schemas"; +export * from "./auth0-connection-types"; diff --git a/backend/src/services/app-connection/databricks/databricks-connection-fns.ts b/backend/src/services/app-connection/databricks/databricks-connection-fns.ts index fc8da062d..a35cc4aec 100644 --- a/backend/src/services/app-connection/databricks/databricks-connection-fns.ts +++ b/backend/src/services/app-connection/databricks/databricks-connection-fns.ts @@ -1,6 +1,7 @@ import { request } from "@app/lib/config/request"; import { BadRequestError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; @@ -26,6 +27,8 @@ const authorizeDatabricksConnection = async ({ clientSecret, workspaceUrl }: Pick) => { + await blockLocalAndPrivateIpAddresses(workspaceUrl); + const { data } = await request.post( `${removeTrailingSlash(workspaceUrl)}/oidc/v1/token`, "grant_type=client_credentials&scope=all-apis", diff --git a/backend/src/services/app-connection/databricks/databricks-connection-schemas.ts b/backend/src/services/app-connection/databricks/databricks-connection-schemas.ts index d876b9749..2ac58b070 100644 --- a/backend/src/services/app-connection/databricks/databricks-connection-schemas.ts +++ b/backend/src/services/app-connection/databricks/databricks-connection-schemas.ts @@ -16,7 +16,7 @@ export const DatabricksConnectionServicePrincipalInputCredentialsSchema = z.obje workspaceUrl: z.string().trim().url().min(1, "Workspace URL required") }); -export const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z +const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z .object({ accessToken: z.string(), expiresAt: z.number() diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 8dfe69643..0e0f999dd 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -2,7 +2,7 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { Knex } from "knex"; -import { TUsers, UserDeviceSchema } from "@app/db/schemas"; +import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; @@ -174,20 +174,25 @@ export const authLoginServiceFactory = ({ const userEnc = await userDAL.findUserEncKeyByUsername({ username: email }); + const serverCfg = await getServerCfg(); + if (!userEnc || (userEnc && !userEnc.isAccepted)) { + throw new Error("Failed to find user"); + } + if ( serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) && !providerAuthToken ) { - throw new BadRequestError({ - message: "Login with email is disabled by administrator." - }); - } - - if (!userEnc || (userEnc && !userEnc.isAccepted)) { - throw new Error("Failed to find user"); + // bypass server configuration when user is an organization admin - this is to prevent lockout + const userOrgs = await orgDAL.findAllOrgsByUserId(userEnc.userId); + if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) { + throw new BadRequestError({ + message: "Login with email is disabled by administrator." + }); + } } if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) { @@ -573,28 +578,40 @@ export const authLoginServiceFactory = ({ switch (authMethod) { case AuthMethod.GITHUB: { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) { - throw new BadRequestError({ - message: "Login with Github is disabled by administrator.", - name: "Oauth 2 login" - }); + // bypass server configuration when user is an organization admin - this is to prevent lockout + const userOrgs = await orgDAL.findAllOrgsByUserId(user.id); + if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) { + throw new BadRequestError({ + message: "Login with Github is disabled by administrator.", + name: "Oauth 2 login" + }); + } } break; } case AuthMethod.GOOGLE: { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GOOGLE)) { - throw new BadRequestError({ - message: "Login with Google is disabled by administrator.", - name: "Oauth 2 login" - }); + // bypass server configuration when user is an organization admin - this is to prevent lockout + const userOrgs = await orgDAL.findAllOrgsByUserId(user.id); + if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) { + throw new BadRequestError({ + message: "Login with Google is disabled by administrator.", + name: "Oauth 2 login" + }); + } } break; } case AuthMethod.GITLAB: { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITLAB)) { - throw new BadRequestError({ - message: "Login with Gitlab is disabled by administrator.", - name: "Oauth 2 login" - }); + // bypass server configuration when user is an organization admin - this is to prevent lockout + const userOrgs = await orgDAL.findAllOrgsByUserId(user.id); + if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) { + throw new BadRequestError({ + message: "Login with Gitlab is disabled by administrator.", + name: "Oauth 2 login" + }); + } } break; } diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 85d348854..02bf58321 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -96,7 +96,9 @@ export const orgDALFactory = (db: TDbClient) => { }; // special query - const findAllOrgsByUserId = async (userId: string): Promise<(TOrganizations & { orgAuthMethod: string })[]> => { + const findAllOrgsByUserId = async ( + userId: string + ): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string })[]> => { try { const org = (await db .replicaNode()(TableName.OrgMembership) @@ -117,6 +119,7 @@ export const orgDALFactory = (db: TDbClient) => { ); }) .select(selectAllTableCols(TableName.Organization)) + .select(db.ref("role").withSchema(TableName.OrgMembership).as("userRole")) .select( db.raw(` CASE @@ -125,7 +128,7 @@ export const orgDALFactory = (db: TDbClient) => { ELSE '' END as "orgAuthMethod" `) - )) as (TOrganizations & { orgAuthMethod: string })[]; + )) as (TOrganizations & { orgAuthMethod: string; userRole: string })[]; return org; } catch (error) { diff --git a/backend/src/services/org/org-schema.ts b/backend/src/services/org/org-schema.ts index ef49d8178..2aa793c04 100644 --- a/backend/src/services/org/org-schema.ts +++ b/backend/src/services/org/org-schema.ts @@ -16,5 +16,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({ allowSecretSharingOutsideOrganization: true, shouldUseNewPrivilegeSystem: true, privilegeUpgradeInitiatedByUsername: true, - privilegeUpgradeInitiatedAt: true + privilegeUpgradeInitiatedAt: true, + bypassOrgAuthEnabled: true }); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 98b35f68f..c83a1a802 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -349,7 +349,8 @@ export const orgServiceFactory = ({ defaultMembershipRoleSlug, enforceMfa, selectedMfaMethod, - allowSecretSharingOutsideOrganization + allowSecretSharingOutsideOrganization, + bypassOrgAuthEnabled } }: TUpdateOrgDTO) => { const appCfg = getConfig(); @@ -429,7 +430,8 @@ export const orgServiceFactory = ({ defaultMembershipRole, enforceMfa, selectedMfaMethod, - allowSecretSharingOutsideOrganization + allowSecretSharingOutsideOrganization, + bypassOrgAuthEnabled }); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); return org; diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 9d14b092e..8a1698015 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -73,6 +73,7 @@ export type TUpdateOrgDTO = { enforceMfa: boolean; selectedMfaMethod: MfaMethod; allowSecretSharingOutsideOrganization: boolean; + bypassOrgAuthEnabled: boolean; }>; } & TOrgPermission; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 1f45734b3..9b7c29c1b 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -86,6 +86,7 @@ import { TProjectAccessRequestDTO, TSearchProjectsDTO, TToggleProjectAutoCapitalizationDTO, + TToggleProjectDeleteProtectionDTO, TUpdateAuditLogsRetentionDTO, TUpdateProjectDTO, TUpdateProjectKmsDTO, @@ -482,6 +483,12 @@ export const projectServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); + if (project.hasDeleteProtection) { + throw new ForbiddenRequestError({ + message: "Project delete protection is enabled" + }); + } + const deletedProject = await projectDAL.transaction(async (tx) => { // delete these so that project custom roles can be deleted in cascade effect // direct deletion of project without these will cause fk error @@ -616,6 +623,7 @@ export const projectServiceFactory = ({ description: update.description, autoCapitalization: update.autoCapitalization, enforceCapitalization: update.autoCapitalization, + hasDeleteProtection: update.hasDeleteProtection, slug: update.slug }); @@ -648,6 +656,29 @@ export const projectServiceFactory = ({ return updatedProject; }; + const toggleDeleteProtection = async ({ + projectId, + actor, + actorId, + actorOrgId, + actorAuthMethod, + hasDeleteProtection + }: TToggleProjectDeleteProtectionDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); + + const updatedProject = await projectDAL.updateById(projectId, { hasDeleteProtection }); + + return updatedProject; + }; + const updateVersionLimit = async ({ actor, actorId, @@ -1499,6 +1530,7 @@ export const projectServiceFactory = ({ getProjectUpgradeStatus, getAProject, toggleAutoCapitalization, + toggleDeleteProtection, updateName, upgradeProject, listProjectCas, diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 4346ae2c4..444f6309c 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -66,6 +66,10 @@ export type TToggleProjectAutoCapitalizationDTO = { autoCapitalization: boolean; } & TProjectPermission; +export type TToggleProjectDeleteProtectionDTO = { + hasDeleteProtection: boolean; +} & TProjectPermission; + export type TUpdateProjectVersionLimitDTO = { pitVersionLimit: number; workspaceSlug: string; @@ -86,6 +90,7 @@ export type TUpdateProjectDTO = { name?: string; description?: string; autoCapitalization?: boolean; + hasDeleteProtection?: boolean; slug?: string; }; } & Omit; diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index f0d579cf7..32f180636 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -10,6 +10,7 @@ import { TSecretVersionDALFactory } from "../secret/secret-version-dal"; import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal"; import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; +import { TServiceTokenServiceFactory } from "../service-token/service-token-service"; type TDailyResourceCleanUpQueueServiceFactoryDep = { auditLogDAL: Pick; @@ -21,6 +22,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = { secretFolderVersionDAL: Pick; snapshotDAL: Pick; secretSharingDAL: Pick; + serviceTokenService: Pick; queueService: TQueueServiceFactory; }; @@ -36,7 +38,8 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ identityAccessTokenDAL, secretSharingDAL, secretVersionV2DAL, - identityUniversalAuthClientSecretDAL + identityUniversalAuthClientSecretDAL, + serviceTokenService }: TDailyResourceCleanUpQueueServiceFactoryDep) => { queueService.start(QueueName.DailyResourceCleanUp, async () => { logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`); @@ -50,6 +53,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ await secretVersionDAL.pruneExcessVersions(); await secretVersionV2DAL.pruneExcessVersions(); await secretFolderVersionDAL.pruneExcessVersions(); + await serviceTokenService.notifyExpiringTokens(); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); }); diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 571ae8b73..d21003b10 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -819,10 +819,14 @@ export const secretImportServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) - ); + if ( + permission.cannot( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + ) + ) { + return []; + } const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) return []; diff --git a/backend/src/services/service-token/service-token-dal.ts b/backend/src/services/service-token/service-token-dal.ts index ed9c5de7e..adb2f325a 100644 --- a/backend/src/services/service-token/service-token-dal.ts +++ b/backend/src/services/service-token/service-token-dal.ts @@ -28,5 +28,36 @@ export const serviceTokenDALFactory = (db: TDbClient) => { } }; - return { ...stOrm, findById }; + const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0) => { + try { + const batch: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] = + await (tx || db.replicaNode())(TableName.ServiceToken) + .leftJoin( + TableName.Users, + `${TableName.Users}.id`, + db.raw(`${TableName.ServiceToken}."createdBy"::uuid`) + ) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`) + .whereRaw( + `${TableName.ServiceToken}."expiresAt" < NOW() + INTERVAL '1 day' AND ${TableName.ServiceToken}."expiryNotificationSent" = false` + ) + .whereNotNull(`${TableName.Users}.email`) + .select( + db.ref("id").withSchema(TableName.ServiceToken), + db.ref("name").withSchema(TableName.ServiceToken), + db.ref("projectId").withSchema(TableName.ServiceToken), + db.ref("createdBy").withSchema(TableName.ServiceToken), + db.ref("email").withSchema(TableName.Users).as("createdByEmail"), + db.ref("name").withSchema(TableName.Project).as("projectName") + ) + .limit(batchSize) + .offset(offset); + + return batch; + } catch (err) { + throw new DatabaseError({ error: err, name: "FindExpiredTokens" }); + } + }; + + return { ...stOrm, findById, findExpiringTokens }; }; diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 9b87c29f8..bbd306bb5 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -12,11 +12,13 @@ import { } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue"; import { ActorType } from "../auth/auth-type"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; +import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { TServiceTokenDALFactory } from "./service-token-dal"; import { @@ -33,6 +35,7 @@ type TServiceTokenServiceFactoryDep = { projectEnvDAL: Pick; projectDAL: Pick; accessTokenQueue: Pick; + smtpService: Pick; }; export type TServiceTokenServiceFactory = ReturnType; @@ -43,7 +46,8 @@ export const serviceTokenServiceFactory = ({ permissionService, projectEnvDAL, projectDAL, - accessTokenQueue + accessTokenQueue, + smtpService }: TServiceTokenServiceFactoryDep) => { const createServiceToken = async ({ iv, @@ -185,11 +189,56 @@ export const serviceTokenServiceFactory = ({ return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId }; }; + const notifyExpiringTokens = async () => { + const appCfg = getConfig(); + let processedCount = 0; + let hasMoreRecords = true; + let offset = 0; + const batchSize = 500; + + while (hasMoreRecords) { + // eslint-disable-next-line no-await-in-loop + const expiringTokens = await serviceTokenDAL.findExpiringTokens(undefined, batchSize, offset); + + if (expiringTokens.length === 0) { + hasMoreRecords = false; + break; + } + + // eslint-disable-next-line no-await-in-loop + await Promise.all( + expiringTokens.map(async (token) => { + try { + await smtpService.sendMail({ + recipients: [token.createdByEmail], + subjectLine: "Service Token Expiry Notice", + template: SmtpTemplates.ServiceTokenExpired, + substitutions: { + tokenName: token.name, + projectName: token.projectName, + url: `${appCfg.SITE_URL}/secret-manager/${token.projectId}/access-management?selectedTab=service-tokens` + } + }); + await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true }); + } catch (error) { + logger.error(error, `Failed to send expiration notification for token ${token.id}:`); + } + }) + ); + + processedCount += expiringTokens.length; + offset += batchSize; + } + + return processedCount; + }; + return { createServiceToken, deleteServiceToken, getServiceToken, getProjectServiceTokens, - fnValidateServiceToken + fnValidateServiceToken, + notifyExpiringTokens }; }; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 452283235..25f5f3949 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -43,7 +43,8 @@ export enum SmtpTemplates { SecretRequestCompleted = "secretRequestCompleted.handlebars", SecretRotationFailed = "secretRotationFailed.handlebars", ProjectAccessRequest = "projectAccess.handlebars", - OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars" + OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars", + ServiceTokenExpired = "serviceTokenExpired.handlebars" } export enum SmtpHost { diff --git a/backend/src/services/smtp/templates/serviceTokenExpired.handlebars b/backend/src/services/smtp/templates/serviceTokenExpired.handlebars new file mode 100644 index 000000000..199150c05 --- /dev/null +++ b/backend/src/services/smtp/templates/serviceTokenExpired.handlebars @@ -0,0 +1,19 @@ + + + + + + Service Token Expiring Soon + + + +

Service Token Expiry Notice

+

Your service token "{{tokenName}}" will expire within 24 hours.

+ +

This token is currently being used on project "{{projectName}}". If this token is still needed for your workflow, please create a new one before it expires.

+ + Create New Token + + {{emailFooter}} + + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/auth0/available.mdx b/docs/api-reference/endpoints/app-connections/auth0/available.mdx new file mode 100644 index 000000000..6694976dc --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/auth0/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/auth0/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/auth0/create.mdx b/docs/api-reference/endpoints/app-connections/auth0/create.mdx new file mode 100644 index 000000000..11e003163 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/auth0/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/auth0" +--- + + + Check out the configuration docs for [Auth0 Connections](/integrations/app-connections/auth0) to learn how to obtain the + required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/auth0/delete.mdx b/docs/api-reference/endpoints/app-connections/auth0/delete.mdx new file mode 100644 index 000000000..f1e79e125 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/auth0/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/auth0/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/auth0/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/auth0/get-by-id.mdx new file mode 100644 index 000000000..0b3a1d355 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/auth0/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/auth0/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/auth0/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/auth0/get-by-name.mdx new file mode 100644 index 000000000..691791523 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/auth0/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/auth0/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/auth0/list.mdx b/docs/api-reference/endpoints/app-connections/auth0/list.mdx new file mode 100644 index 000000000..9590b0487 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/auth0/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/auth0" +--- diff --git a/docs/api-reference/endpoints/app-connections/auth0/update.mdx b/docs/api-reference/endpoints/app-connections/auth0/update.mdx new file mode 100644 index 000000000..e7046ba19 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/auth0/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/auth0/{connectionId}" +--- + + + Check out the configuration docs for [Auth0 Connections](/integrations/app-connections/auth0) to learn how to obtain the + required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/create.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/create.mdx new file mode 100644 index 000000000..c279da181 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v2/secret-rotations/auth0-client-secret" +--- + + + Check out the configuration docs for [Auth0 Client Secret Rotations](/documentation/platform/secret-rotation/auth0-client-secret) to learn how to obtain the + required parameters. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/delete.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/delete.mdx new file mode 100644 index 000000000..8cf4227d7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v2/secret-rotations/auth0-client-secret/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-id.mdx new file mode 100644 index 000000000..60d9ad0a1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v2/secret-rotations/auth0-client-secret/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-name.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-name.mdx new file mode 100644 index 000000000..e513f74ec --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v2/secret-rotations/auth0-client-secret/rotation-name/{rotationName}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-generated-credentials-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-generated-credentials-by-id.mdx new file mode 100644 index 000000000..a4489053f --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-generated-credentials-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Credentials by ID" +openapi: "GET /api/v2/secret-rotations/auth0-client-secret/{rotationId}/generated-credentials" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/list.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/list.mdx new file mode 100644 index 000000000..a1b1a70cc --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/secret-rotations/auth0-client-secret" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/rotate-secrets.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/rotate-secrets.mdx new file mode 100644 index 000000000..45349ca30 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/rotate-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Rotate Secrets" +openapi: "POST /api/v2/secret-rotations/auth0-client-secret/{rotationId}/rotate-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/update.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/update.mdx new file mode 100644 index 000000000..514730100 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v2/secret-rotations/auth0-client-secret/{rotationId}" +--- + + + Check out the configuration docs for [Auth0 Client Secret Rotations](/documentation/platform/secret-rotation/auth0-client-secret) to learn how to obtain the + required parameters. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-rotations/mssql-credentials/create.mdx b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/create.mdx index 8b6c8bc88..5ed8c08b9 100644 --- a/docs/api-reference/endpoints/secret-rotations/mssql-credentials/create.mdx +++ b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/create.mdx @@ -5,6 +5,6 @@ openapi: "POST /api/v2/secret-rotations/mssql-credentials" Check out the configuration docs for [Microsoft SQL Server - Credentials Rotations](/documentation/platform/secret-rotation/mssql) to learn how to obtain the + Credentials Rotations](/documentation/platform/secret-rotation/mssql-credentials) to learn how to obtain the required parameters. diff --git a/docs/api-reference/endpoints/secret-rotations/mssql-credentials/update.mdx b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/update.mdx index 4dd5f3267..027d83a1f 100644 --- a/docs/api-reference/endpoints/secret-rotations/mssql-credentials/update.mdx +++ b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/update.mdx @@ -5,6 +5,6 @@ openapi: "PATCH /api/v2/secret-rotations/mssql-credentials/{rotationId}" Check out the configuration docs for [Microsoft SQL Server - Credentials Rotations](/documentation/platform/secret-rotation/mssql) to learn how to obtain the + Credentials Rotations](/documentation/platform/secret-rotation/mssql-credentials) to learn how to obtain the required parameters. diff --git a/docs/api-reference/endpoints/secret-rotations/postgres-credentials/create.mdx b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/create.mdx index fabd51f94..e22b89f81 100644 --- a/docs/api-reference/endpoints/secret-rotations/postgres-credentials/create.mdx +++ b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/create.mdx @@ -5,6 +5,6 @@ openapi: "POST /api/v2/secret-rotations/postgres-credentials" Check out the configuration docs for [PostgreSQL - Credentials Rotations](/documentation/platform/secret-rotation/postgres) to learn how to obtain the + Credentials Rotations](/documentation/platform/secret-rotation/postgres-credentials) to learn how to obtain the required parameters. \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-rotations/postgres-credentials/update.mdx b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/update.mdx index 7aebcb72c..46438427f 100644 --- a/docs/api-reference/endpoints/secret-rotations/postgres-credentials/update.mdx +++ b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/update.mdx @@ -5,6 +5,6 @@ openapi: "PATCH /api/v2/secret-rotations/postgres-credentials/{rotationId}" Check out the configuration docs for [PostgreSQL - Credentials Rotations](/documentation/platform/secret-rotation/postgres) to learn how to obtain the + Credentials Rotations](/documentation/platform/secret-rotation/postgres-credentials) to learn how to obtain the required parameters. \ No newline at end of file diff --git a/docs/changelog/overview.mdx b/docs/changelog/overview.mdx index 822a8b24a..6d1440ff1 100644 --- a/docs/changelog/overview.mdx +++ b/docs/changelog/overview.mdx @@ -173,7 +173,7 @@ The changelog below reflects new product developments and updates on a monthly b - Replaced internal [Winston](https://github.com/winstonjs/winston) with [Pino](https://github.com/pinojs/pino) logging library with external logging to AWS CloudWatch - Added admin panel to self-hosting experience. -- Released [secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview) feature with preliminary support for rotating [SendGrid](https://infisical.com/docs/documentation/platform/secret-rotation/sendgrid), [PostgreSQL/CockroachDB](https://infisical.com/docs/documentation/platform/secret-rotation/postgres), and [MySQL/MariaDB](https://infisical.com/docs/documentation/platform/secret-rotation/mysql) credentials. +- Released [secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview) feature with preliminary support for rotating [SendGrid](https://infisical.com/docs/documentation/platform/secret-rotation/sendgrid), [PostgreSQL/CockroachDB](https://infisical.com/docs/documentation/platform/secret-rotation/postgres-credentials), and [MySQL/MariaDB](https://infisical.com/docs/documentation/platform/secret-rotation/mysql) credentials. - Released secret reminders feature. ## Oct 2023 diff --git a/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx b/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx new file mode 100644 index 000000000..3845a3879 --- /dev/null +++ b/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx @@ -0,0 +1,154 @@ +--- +title: "Auth0 Client Secret" +description: "Learn how to automatically rotate Auth0 Client Secrets." +--- + + + Due to how Auth0 client secrets are rotated, retired credentials will not be able to + authenticate with Auth0 during their [inactive period](./overview#how-rotation-works). + + This is a limitation of the Auth0 platform and cannot be + rectified by Infisical. + + +## Prerequisites + +- Create an [Auth0 Connection](/integrations/app-connections/auth0) with the required **Secret Rotation** audience and permissions + +## Create an Auth0 Client Secret Rotation in Infisical + + + + 1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown. + ![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png) + + 2. Select the **Auth0 Client Secret** option. + ![Select Auth0 Client Secret](/images/secret-rotations-v2/auth0-client-secret/select-auth0-client-secret-option.png) + + 3. Select the **Auth0 Connection** to use and configure the rotation behavior. Then click **Next**. + ![Rotation Configuration](/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-configuration.png) + + - **Auth0 Connection** - the connection that will perform the rotation of the specified application's Client Secret. + - **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation. + - **Rotate At** - the local time of day when rotation should occur once the interval has elapsed. + - **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation. + + Due to Auth0 Client Secret Rotations rotating a single credential set, auto-rotation may result in service interruptions. If you need to ensure service continuity, we recommend disabling this option. + + + + 4. Select the Auth0 application whose Client Secret you want to rotate. Then click **Next**. + ![Rotation Parameters](/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-parameters.png) + + 5. Specify the secret names that the client credentials should be mapped to. Then click **Next**. + ![Rotation Secrets Mapping](/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-secrets-mapping.png) + + - **Client ID** - the name of the secret that the application Client ID will be mapped to. + - **Client Secret** - the name of the secret that the rotated Client Secret will be mapped to. + + 6. Give your rotation a name and description (optional). Then click **Next**. + ![Rotation Details](/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-details.png) + + - **Name** - the name of the secret rotation configuration. Must be slug-friendly. + - **Description** (optional) - a description of this rotation configuration. + + 7. Review your configuration, then click **Create Secret Rotation**. + ![Rotation Review](/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-confirm.png) + + 8. Your **Auth0 Client Secret** credentials are now available for use via the mapped secrets. + ![Rotation Created](/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-created.png) + + + To create an Auth0 Client Secret Rotation, make an API request to the [Create Auth0 + Client Secret Rotation](/api-reference/endpoints/secret-rotations/auth0-client-secret/create) API endpoint. + + You will first need the **Client ID** of the Auth0 application you want to rotate the secret for. This can be obtained from the Applications dashboard. + ![Auth0 Client ID](/images/secret-rotations-v2/auth0-client-secret/auth0-app-client-id.png) + + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://us.infisical.com/api/v2/secret-rotations/auth0-client-secret \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-auth0-rotation", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "my client secret rotation", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/", + "isAutoRotationEnabled": true, + "rotationInterval": 30, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "parameters": { + "clientId": "...", + }, + "secretsMapping": { + "clientId": "AUTH0_CLIENT_ID", + "clientSecret": "AUTH0_CLIENT_SECRET" + } + }' + ``` + + + Due to Auth0 Client Secret Rotations rotating a single credential set, auto-rotation may result in service interruptions. If you need to ensure service continuity, we recommend disabling this option. + + + ### Sample response + + ```bash Response + { + "secretRotation": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-auth0-rotation", + "description": "my client secret rotation", + "secretsMapping": { + "clientId": "AUTH0_CLIENT_ID", + "clientSecret": "AUTH0_CLIENT_SECRET" + }, + "isAutoRotationEnabled": true, + "activeIndex": 0, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "rotationInterval": 30, + "rotationStatus": "success", + "lastRotationAttemptedAt": "2023-11-07T05:31:56Z", + "lastRotatedAt": "2023-11-07T05:31:56Z", + "lastRotationJobId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "nextRotationAt": "2023-11-07T05:31:56Z", + "connection": { + "app": "auth0", + "name": "my-auth0-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/" + }, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "lastRotationMessage": null, + "type": "auth0-client-secret", + "parameters": { + "clientId": "...", + } + } + } + ``` + + diff --git a/docs/documentation/platform/secret-rotation/mssql.mdx b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx similarity index 100% rename from docs/documentation/platform/secret-rotation/mssql.mdx rename to docs/documentation/platform/secret-rotation/mssql-credentials.mdx diff --git a/docs/documentation/platform/secret-rotation/overview.mdx b/docs/documentation/platform/secret-rotation/overview.mdx index ee1fd9e42..d11334440 100644 --- a/docs/documentation/platform/secret-rotation/overview.mdx +++ b/docs/documentation/platform/secret-rotation/overview.mdx @@ -47,6 +47,11 @@ Each set of credentials transitions through three distinct states: - **Active**: The primary credentials that will be used for new connections - **Inactive**: These credentials are still valid but are no longer issued for new connections + + Some rotation providers utilize a single credential set due to technical constraints. As a result, inactive credentials for these providers will immediately become invalid once rotated. + + To avoid service interruptions, Infisical recommends manually rotating these credentials to prevent downtime. + - **Revoked**: Permanently invalidated and deleted from the system ### Rotation Cycle Example (30-Day Interval) @@ -95,3 +100,16 @@ Using a __30-Day__ rotation interval as an example, here's how the process unfol - [PostgreSQL Credentials](./postgres) - [Microsoft SQL Server Credentials](./mssql) + +## FAQ + + + + Some credential providers have limitations that affect rotation patterns: + + - The third-party provider's API only supports managing one active credential set at a time + - The specific use-case (such as personal login accounts) is inherently limited to a single active credential + + In either scenario, when service continuity is critical, Infisical recommends disabling auto-rotation and performing manual credential rotation during scheduled maintenance windows. + + diff --git a/docs/documentation/platform/secret-rotation/postgres.mdx b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx similarity index 100% rename from docs/documentation/platform/secret-rotation/postgres.mdx rename to docs/documentation/platform/secret-rotation/postgres-credentials.mdx diff --git a/docs/documentation/platform/sso/auth0-oidc.mdx b/docs/documentation/platform/sso/auth0-oidc.mdx index 9419d0976..bde87f42f 100644 --- a/docs/documentation/platform/sso/auth0-oidc.mdx +++ b/docs/documentation/platform/sso/auth0-oidc.mdx @@ -42,7 +42,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO." 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Connect**. ![OIDC auth0 manage org Infisical](../../../images/sso/auth0-oidc/org-oidc-overview.png) - 3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **Client ID**, and **Client Secret** from step 2.1 and 2.2. + 3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret** from step 2.1 and 2.2. ![OIDC auth0 paste values into Infisical](../../../images/sso/auth0-oidc/org-update-oidc.png) Once you've done that, press **Update** to complete the required configuration. @@ -65,7 +65,9 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO." We recommend ensuring that your account is provisioned using the application in Auth0 prior to enforcing OIDC SSO to prevent any unintended issues. - + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/auth0-saml.mdx b/docs/documentation/platform/sso/auth0-saml.mdx index b77c733f7..b426d1aae 100644 --- a/docs/documentation/platform/sso/auth0-saml.mdx +++ b/docs/documentation/platform/sso/auth0-saml.mdx @@ -72,6 +72,10 @@ description: "Learn how to configure Auth0 SAML for Infisical SSO." To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Auth0 user with Infisical; Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO. + + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/azure.mdx b/docs/documentation/platform/sso/azure.mdx index 21236cd5b..282cddae5 100644 --- a/docs/documentation/platform/sso/azure.mdx +++ b/docs/documentation/platform/sso/azure.mdx @@ -106,6 +106,9 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO." We recommend ensuring that your account is provisioned the application in Azure prior to enforcing SAML SSO to prevent any unintended issues. + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/general-oidc.mdx b/docs/documentation/platform/sso/general-oidc.mdx index 7e3a76ff0..11216b893 100644 --- a/docs/documentation/platform/sso/general-oidc.mdx +++ b/docs/documentation/platform/sso/general-oidc.mdx @@ -66,6 +66,9 @@ Prerequisites: We recommend ensuring that your account is provisioned using the identity provider prior to enforcing OIDC SSO to prevent any unintended issues. + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/google-saml.mdx b/docs/documentation/platform/sso/google-saml.mdx index 7e47d1137..87ffa8412 100644 --- a/docs/documentation/platform/sso/google-saml.mdx +++ b/docs/documentation/platform/sso/google-saml.mdx @@ -81,6 +81,9 @@ description: "Learn how to configure Google SAML for Infisical SSO." We recommend ensuring that your account is provisioned the application in Google prior to enforcing SAML SSO to prevent any unintended issues. + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/jumpcloud.mdx b/docs/documentation/platform/sso/jumpcloud.mdx index ec876b9da..6ca20c752 100644 --- a/docs/documentation/platform/sso/jumpcloud.mdx +++ b/docs/documentation/platform/sso/jumpcloud.mdx @@ -86,6 +86,9 @@ description: "Learn how to configure JumpCloud SAML for Infisical SSO." We recommend ensuring that your account is provisioned the application in JumpCloud prior to enforcing SAML SSO to prevent any unintended issues. + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx index 4f5bc689e..6d8f4e4c8 100644 --- a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx +++ b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx @@ -69,7 +69,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO." 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect. ![OIDC keycloak manage org Infisical](/images/sso/keycloak-oidc/manage-org-oidc.png) - 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**. + 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret**. ![OIDC keycloak paste values into Infisical](/images/sso/keycloak-oidc/create-oidc.png) Once you've done that, press **Update** to complete the required configuration. @@ -92,7 +92,9 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO." We recommend ensuring that your account is provisioned using the application in Keycloak prior to enforcing OIDC SSO to prevent any unintended issues. - + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/keycloak-saml.mdx b/docs/documentation/platform/sso/keycloak-saml.mdx index 86352bb9d..7e4004122 100644 --- a/docs/documentation/platform/sso/keycloak-saml.mdx +++ b/docs/documentation/platform/sso/keycloak-saml.mdx @@ -127,6 +127,9 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO." We recommend ensuring that your account is provisioned the application in Keycloak prior to enforcing SAML SSO to prevent any unintended issues. + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/okta.mdx b/docs/documentation/platform/sso/okta.mdx index 9f28f4c4e..1abd03d6f 100644 --- a/docs/documentation/platform/sso/okta.mdx +++ b/docs/documentation/platform/sso/okta.mdx @@ -94,6 +94,9 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO." We recommend ensuring that your account is provisioned the application in Okta prior to enforcing SAML SSO to prevent any unintended issues. + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/images/app-connections/auth0/auth0-audience.png b/docs/images/app-connections/auth0/auth0-audience.png new file mode 100644 index 000000000..1c5226aac Binary files /dev/null and b/docs/images/app-connections/auth0/auth0-audience.png differ diff --git a/docs/images/app-connections/auth0/auth0-client-credentials.png b/docs/images/app-connections/auth0/auth0-client-credentials.png new file mode 100644 index 000000000..3fea1096f Binary files /dev/null and b/docs/images/app-connections/auth0/auth0-client-credentials.png differ diff --git a/docs/images/app-connections/auth0/auth0-dashboard-applications.png b/docs/images/app-connections/auth0/auth0-dashboard-applications.png new file mode 100644 index 000000000..225113a04 Binary files /dev/null and b/docs/images/app-connections/auth0/auth0-dashboard-applications.png differ diff --git a/docs/images/app-connections/auth0/auth0-secret-rotation-api-selection.png b/docs/images/app-connections/auth0/auth0-secret-rotation-api-selection.png new file mode 100644 index 000000000..42f3ee1e4 Binary files /dev/null and b/docs/images/app-connections/auth0/auth0-secret-rotation-api-selection.png differ diff --git a/docs/images/app-connections/auth0/auth0-select-m2m.png b/docs/images/app-connections/auth0/auth0-select-m2m.png new file mode 100644 index 000000000..2d83c6de9 Binary files /dev/null and b/docs/images/app-connections/auth0/auth0-select-m2m.png differ diff --git a/docs/images/app-connections/auth0/client-credentials-create.png b/docs/images/app-connections/auth0/client-credentials-create.png new file mode 100644 index 000000000..2c4466cd2 Binary files /dev/null and b/docs/images/app-connections/auth0/client-credentials-create.png differ diff --git a/docs/images/app-connections/auth0/client_credentials_connection.png b/docs/images/app-connections/auth0/client_credentials_connection.png new file mode 100644 index 000000000..a4b115523 Binary files /dev/null and b/docs/images/app-connections/auth0/client_credentials_connection.png differ diff --git a/docs/images/app-connections/auth0/select-auth0-connection.png b/docs/images/app-connections/auth0/select-auth0-connection.png new file mode 100644 index 000000000..47d72d2a5 Binary files /dev/null and b/docs/images/app-connections/auth0/select-auth0-connection.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/auth0-app-client-id.png b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-app-client-id.png new file mode 100644 index 000000000..5540b7312 Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-app-client-id.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-configuration.png b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-configuration.png new file mode 100644 index 000000000..f254c244c Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-configuration.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-confirm.png b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-confirm.png new file mode 100644 index 000000000..1dcd4715c Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-confirm.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-created.png b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-created.png new file mode 100644 index 000000000..fd82360da Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-created.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-details.png b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-details.png new file mode 100644 index 000000000..42c49f808 Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-details.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-parameters.png b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-parameters.png new file mode 100644 index 000000000..1f2fc64f9 Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-parameters.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-secrets-mapping.png b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-secrets-mapping.png new file mode 100644 index 000000000..c91e54559 Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-secrets-mapping.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/select-auth0-client-secret-option.png b/docs/images/secret-rotations-v2/auth0-client-secret/select-auth0-client-secret-option.png new file mode 100644 index 000000000..d042f46fb Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/select-auth0-client-secret-option.png differ diff --git a/docs/images/sso/auth0-oidc/org-update-oidc.png b/docs/images/sso/auth0-oidc/org-update-oidc.png index 0b9e96b5b..bd61584a5 100644 Binary files a/docs/images/sso/auth0-oidc/org-update-oidc.png and b/docs/images/sso/auth0-oidc/org-update-oidc.png differ diff --git a/docs/images/sso/keycloak-oidc/create-oidc.png b/docs/images/sso/keycloak-oidc/create-oidc.png index 358af1330..bf8aceb05 100644 Binary files a/docs/images/sso/keycloak-oidc/create-oidc.png and b/docs/images/sso/keycloak-oidc/create-oidc.png differ diff --git a/docs/integrations/app-connections/auth0.mdx b/docs/integrations/app-connections/auth0.mdx new file mode 100644 index 000000000..42e78cb66 --- /dev/null +++ b/docs/integrations/app-connections/auth0.mdx @@ -0,0 +1,101 @@ +--- +title: "Auth0 Connection" +description: "Learn how to configure an Auth0 Connection for Infisical." +--- + +Infisical supports the use of [Client Credentials](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow) to connect with your Auth0 applications. + +## Configure a Machine-to-Machine Application in Auth0 + + + + Navigate to the **Applications** page in Auth0 via the sidebar and click **Create Application**. + ![Applications Page](/images/app-connections/auth0/auth0-dashboard-applications.png) + + + Give your application a name and select **Machine-to-Machine** for the application type. + + ![Create Machine-to-Machine Application](/images/app-connections/auth0/auth0-select-m2m.png) + + + Depending on your connection use case, authorize your application for the applicable API and grant the relevant permissions. Once done, click **Authorize**. + + + + Select the **Auth0 Management API** option from the dropdown and grant the `update:client_keys` and `read:clients` permission. + ![Secret Rotation Authorization](/images/app-connections/auth0/auth0-secret-rotation-api-selection.png) + + + + + On your application page, select the **Settings** tab and copy the **Domain**, **Client ID** and **Client Secret** for later. + + ![Client Credentials](/images/app-connections/auth0/auth0-client-credentials.png) + + + Next, select the **APIs** tab and copy the **API Identifier**. + ![Application Audience](/images/app-connections/auth0/auth0-audience.png) + + + +## Setup Auth0 Connection in Infisical + + + + 1. Navigate to the App Connections tab on the Organization Settings page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + 2. Select the **Auth0 Connection** option. + ![Select Auth0 Connection](/images/app-connections/auth0/select-auth0-connection.png) + + 3. Select the **Client Credentials** method option and provide the details obtained from the previous section and press **Connect to Auth0**. + ![Create Auth0 Connection](/images/app-connections/auth0/client-credentials-create.png) + + 4. Your **Auth0 Connection** is now available for use. + ![Assume Role Auth0 Connection](/images/app-connections/auth0/client_credentials_connection.png) + + + To create a Auth0 Connection, make an API request to the [Create Auth0 + Connection](/api-reference/endpoints/app-connections/auth0/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/auth0 \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-auth0-connection", + "method": "client-credentials", + "credentials": { + "domain": "xxx-xxxxxxxxx.us.auth0.com", + "clientId": "...", + "clientSecret": "...", + "audience": "https://xxx-xxxxxxxxx.us.auth0.com/api/v2/" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-auth0-connection", + "version": 1, + "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "app": "auth0", + "method": "client-credentials", + "credentials": { + "domain": "xxx-xxxxxxxxx.us.auth0.com", + "clientId": "...", + "audience": "https://xxx-xxxxxxxxx.us.auth0.com/api/v2/" + } + } + } + ``` + + diff --git a/docs/mint.json b/docs/mint.json index 28b2ab9da..c75e23859 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -178,8 +178,9 @@ "group": "Secret Rotation", "pages": [ "documentation/platform/secret-rotation/overview", - "documentation/platform/secret-rotation/postgres", - "documentation/platform/secret-rotation/mssql" + "documentation/platform/secret-rotation/auth0-client-secret", + "documentation/platform/secret-rotation/postgres-credentials", + "documentation/platform/secret-rotation/mssql-credentials" ] }, { @@ -414,6 +415,7 @@ { "group": "Connections", "pages": [ + "integrations/app-connections/auth0", "integrations/app-connections/aws", "integrations/app-connections/azure-app-configuration", "integrations/app-connections/azure-key-vault", @@ -570,9 +572,7 @@ "sdks/languages/node", "sdks/languages/python", "sdks/languages/java", - "sdks/languages/go", - "sdks/languages/ruby", - "sdks/languages/csharp" + "sdks/languages/go" ] }, { @@ -846,6 +846,19 @@ "pages": [ "api-reference/endpoints/secret-rotations/list", "api-reference/endpoints/secret-rotations/options", + { + "group": "Auth0 Client Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/auth0-client-secret/create", + "api-reference/endpoints/secret-rotations/auth0-client-secret/delete", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-id", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-name", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/auth0-client-secret/list", + "api-reference/endpoints/secret-rotations/auth0-client-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/auth0-client-secret/update" + ] + }, { "group": "Microsoft SQL Server Credentials", "pages": [ @@ -890,6 +903,18 @@ "pages": [ "api-reference/endpoints/app-connections/list", "api-reference/endpoints/app-connections/options", + { + "group": "Auth0", + "pages": [ + "api-reference/endpoints/app-connections/auth0/list", + "api-reference/endpoints/app-connections/auth0/available", + "api-reference/endpoints/app-connections/auth0/get-by-id", + "api-reference/endpoints/app-connections/auth0/get-by-name", + "api-reference/endpoints/app-connections/auth0/create", + "api-reference/endpoints/app-connections/auth0/update", + "api-reference/endpoints/app-connections/auth0/delete" + ] + }, { "group": "AWS", "pages": [ diff --git a/docs/sdks/languages/csharp.mdx b/docs/sdks/languages/csharp.mdx index 344b0a663..55fcd215c 100644 --- a/docs/sdks/languages/csharp.mdx +++ b/docs/sdks/languages/csharp.mdx @@ -1,4 +1,4 @@ ---- +{/* --- title: "Infisical .NET SDK" sidebarTitle: ".NET" icon: "bars" @@ -584,4 +584,4 @@ var decryptedPlaintext = infisical.DecryptSymmetric(decryptOptions); #### Returns (string) `Plaintext` (string): The decrypted plaintext. - + */} diff --git a/docs/sdks/languages/ruby.mdx b/docs/sdks/languages/ruby.mdx index 617594957..5b0e9668a 100644 --- a/docs/sdks/languages/ruby.mdx +++ b/docs/sdks/languages/ruby.mdx @@ -1,4 +1,4 @@ ---- +{/* --- title: "Infisical Ruby SDK" sidebarTitle: "Ruby" icon: "diamond" @@ -433,4 +433,4 @@ decrypted_data = infisical.cryptography.decrypt_symmetric( #### Returns (string) -`Plaintext` (string): The decrypted plaintext. \ No newline at end of file +`Plaintext` (string): The decrypted plaintext. */} \ No newline at end of file diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 103c6400e..8318869f2 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -34,8 +34,14 @@ Used to configure platform-specific security and operational settings this to `false`. - - Determines whether App Connections and Dynamic Secrets are permitted to connect with internal/private IP addresses. + + Determines whether App Connections and Dynamic Secrets are permitted to + connect with internal/private IP addresses. ## CORS @@ -45,24 +51,31 @@ The following environment variables can be used to configure the Infisical Rest - Specify a list of origins that are allowed to access the Infisical API. +Specify a list of origins that are allowed to access the Infisical API. - An example value would be `CORS_ALLOWED_ORIGINS=["https://example.com"]`. +An example value would be `CORS_ALLOWED_ORIGINS=["https://example.com"]`. + +Defaults to the same value as your `SITE_URL` environment variable. - Defaults to the same value as your `SITE_URL` environment variable. Array of HTTP methods allowed for CORS requests. - Defaults to reflecting the headers specified in the request's Access-Control-Request-Headers header. - +Defaults to reflecting the headers specified in the request's Access-Control-Request-Headers header. + ## Data Layer The platform utilizes Postgres to persist all of its data and Redis for caching and backgroud tasks +### PostgreSQL + + + Please note that the database user must have **CREATE** privileges along with ability to create and modify tables. This is needed for Infisical to run schema migrations. + + Postgres database connection string. @@ -73,10 +86,6 @@ The platform utilizes Postgres to persist all of its data and Redis for caching `echo "" | base64` - - Redis connection string. - - Postgres database read replica connection strings. It accepts a JSON string. ``` @@ -97,6 +106,12 @@ DB_READ_REPLICAS=[{"DB_CONNECTION_URI":""}] +### Redis + + + Redis connection string. + + ## Email Service Without email configuration, Infisical's core functions like sign-up/login and secret operations work, but this disables multi-factor authentication, email invites for projects, alerts for suspicious logins, and all other email-dependent features. @@ -455,6 +470,7 @@ You can configure third-party app connections for re-use across Infisical Projec The AWS IAM User secret key for assuming roles + @@ -477,6 +493,7 @@ You can configure third-party app connections for re-use across Infisical Projec The private key for the GitHub App + @@ -487,6 +504,7 @@ You can configure third-party app connections for re-use across Infisical Projec The OAuth2 client secret for GitHub OAuth Connection + ## Native Secret Integrations diff --git a/frontend/public/images/integrations/Auth0.png b/frontend/public/images/integrations/Auth0.png new file mode 100644 index 000000000..e86d76c06 Binary files /dev/null and b/frontend/public/images/integrations/Auth0.png differ diff --git a/frontend/public/images/sso/Auth0.png b/frontend/public/images/sso/Auth0.png new file mode 100644 index 000000000..e86d76c06 Binary files /dev/null and b/frontend/public/images/sso/Auth0.png differ diff --git a/frontend/public/images/sso/Google.png b/frontend/public/images/sso/Google.png new file mode 100644 index 000000000..b3ed76596 Binary files /dev/null and b/frontend/public/images/sso/Google.png differ diff --git a/frontend/public/images/sso/JumpCloud.png b/frontend/public/images/sso/JumpCloud.png new file mode 100644 index 000000000..94d15f71e Binary files /dev/null and b/frontend/public/images/sso/JumpCloud.png differ diff --git a/frontend/public/images/sso/Keycloak.png b/frontend/public/images/sso/Keycloak.png new file mode 100644 index 000000000..86405f3af Binary files /dev/null and b/frontend/public/images/sso/Keycloak.png differ diff --git a/frontend/public/images/sso/Microsoft Azure.png b/frontend/public/images/sso/Microsoft Azure.png new file mode 100644 index 000000000..c9388d612 Binary files /dev/null and b/frontend/public/images/sso/Microsoft Azure.png differ diff --git a/frontend/public/images/sso/Okta.png b/frontend/public/images/sso/Okta.png new file mode 100644 index 000000000..d742d4347 Binary files /dev/null and b/frontend/public/images/sso/Okta.png differ diff --git a/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx b/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx index 8a824d101..210257d7d 100644 --- a/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx +++ b/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx @@ -1,4 +1,6 @@ import { useState } from "react"; +import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SecretRotationV2Form } from "@app/components/secret-rotations-v2/forms"; import { SecretRotationV2ModalHeader } from "@app/components/secret-rotations-v2/SecretRotationV2ModalHeader"; @@ -54,7 +56,24 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }: selectedRotation ? ( ) : ( - "Add Secret Rotation" +
+ Add Secret Rotation + +
+ + Docs + +
+
+
) } onPointerDownOutside={(e) => e.preventDefault()} diff --git a/frontend/src/components/secret-rotations-v2/SecretRotationV2ModalHeader.tsx b/frontend/src/components/secret-rotations-v2/SecretRotationV2ModalHeader.tsx index 319607d70..db51e27ca 100644 --- a/frontend/src/components/secret-rotations-v2/SecretRotationV2ModalHeader.tsx +++ b/frontend/src/components/secret-rotations-v2/SecretRotationV2ModalHeader.tsx @@ -24,7 +24,7 @@ export const SecretRotationV2ModalHeader = ({ type, isConfigured }: Props) => { {destinationDetails.name} Rotation diff --git a/frontend/src/components/secret-rotations-v2/SecretRotationV2Select.tsx b/frontend/src/components/secret-rotations-v2/SecretRotationV2Select.tsx index c464279ed..5679940fd 100644 --- a/frontend/src/components/secret-rotations-v2/SecretRotationV2Select.tsx +++ b/frontend/src/components/secret-rotations-v2/SecretRotationV2Select.tsx @@ -24,17 +24,7 @@ export const SecretRotationV2Select = ({ onSelect }: Props) => { return (
{secretRotationOptions?.map(({ type }) => { - const { image, name } = SECRET_ROTATION_MAP[type]; - - let size: number; - - switch (type) { - case SecretRotation.MsSqlCredentials: - size = 50; - break; - default: - size = 45; - } + const { image, name, size } = SECRET_ROTATION_MAP[type]; return (