diff --git a/backend/src/ee/routes/v1/ldap-router.ts b/backend/src/ee/routes/v1/ldap-router.ts index 57c5736df..7c520e2ab 100644 --- a/backend/src/ee/routes/v1/ldap-router.ts +++ b/backend/src/ee/routes/v1/ldap-router.ts @@ -17,6 +17,7 @@ import { z } from "zod"; import { LdapGroupMapsSchema } from "@app/db/schemas"; import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types"; import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns"; +import { ApiDocsTags, LdapSso } from "@app/lib/api-docs"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; @@ -132,10 +133,18 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { config: { rateLimit: readLimit }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.LdapSso], + description: "Get LDAP config", + security: [ + { + bearerAuth: [] + } + ], querystring: z.object({ - organizationId: z.string().trim() + organizationId: z.string().trim().describe(LdapSso.GET_CONFIG.organizationId) }), response: { 200: z.object({ @@ -172,23 +181,32 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { config: { rateLimit: writeLimit }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.LdapSso], + description: "Create LDAP config", + security: [ + { + bearerAuth: [] + } + ], body: z.object({ - organizationId: z.string().trim(), - isActive: z.boolean(), - url: z.string().trim(), - bindDN: z.string().trim(), - bindPass: z.string().trim(), - uniqueUserAttribute: z.string().trim().default("uidNumber"), - searchBase: z.string().trim(), - searchFilter: z.string().trim().default("(uid={{username}})"), - groupSearchBase: z.string().trim(), + organizationId: z.string().trim().describe(LdapSso.CREATE_CONFIG.organizationId), + isActive: z.boolean().describe(LdapSso.CREATE_CONFIG.isActive), + url: z.string().trim().describe(LdapSso.CREATE_CONFIG.url), + bindDN: z.string().trim().describe(LdapSso.CREATE_CONFIG.bindDN), + bindPass: z.string().trim().describe(LdapSso.CREATE_CONFIG.bindPass), + uniqueUserAttribute: z.string().trim().default("uidNumber").describe(LdapSso.CREATE_CONFIG.uniqueUserAttribute), + searchBase: z.string().trim().describe(LdapSso.CREATE_CONFIG.searchBase), + searchFilter: z.string().trim().default("(uid={{username}})").describe(LdapSso.CREATE_CONFIG.searchFilter), + groupSearchBase: z.string().trim().describe(LdapSso.CREATE_CONFIG.groupSearchBase), groupSearchFilter: z .string() .trim() - .default("(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))"), - caCert: z.string().trim().default("") + .default("(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))") + .describe(LdapSso.CREATE_CONFIG.groupSearchFilter), + caCert: z.string().trim().default("").describe(LdapSso.CREATE_CONFIG.caCert) }), response: { 200: SanitizedLdapConfigSchema @@ -214,23 +232,31 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { config: { rateLimit: writeLimit }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.LdapSso], + description: "Update LDAP config", + security: [ + { + bearerAuth: [] + } + ], body: z .object({ - isActive: z.boolean(), - url: z.string().trim(), - bindDN: z.string().trim(), - bindPass: z.string().trim(), - uniqueUserAttribute: z.string().trim(), - searchBase: z.string().trim(), - searchFilter: z.string().trim(), - groupSearchBase: z.string().trim(), - groupSearchFilter: z.string().trim(), - caCert: z.string().trim() + isActive: z.boolean().describe(LdapSso.UPDATE_CONFIG.isActive), + url: z.string().trim().describe(LdapSso.UPDATE_CONFIG.url), + bindDN: z.string().trim().describe(LdapSso.UPDATE_CONFIG.bindDN), + bindPass: z.string().trim().describe(LdapSso.UPDATE_CONFIG.bindPass), + uniqueUserAttribute: z.string().trim().describe(LdapSso.UPDATE_CONFIG.uniqueUserAttribute), + searchBase: z.string().trim().describe(LdapSso.UPDATE_CONFIG.searchBase), + searchFilter: z.string().trim().describe(LdapSso.UPDATE_CONFIG.searchFilter), + groupSearchBase: z.string().trim().describe(LdapSso.UPDATE_CONFIG.groupSearchBase), + groupSearchFilter: z.string().trim().describe(LdapSso.UPDATE_CONFIG.groupSearchFilter), + caCert: z.string().trim().describe(LdapSso.UPDATE_CONFIG.caCert) }) .partial() - .merge(z.object({ organizationId: z.string() })), + .merge(z.object({ organizationId: z.string().trim().describe(LdapSso.UPDATE_CONFIG.organizationId) })), response: { 200: SanitizedLdapConfigSchema } diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts index 1bfc4d696..59c05272d 100644 --- a/backend/src/ee/routes/v1/oidc-router.ts +++ b/backend/src/ee/routes/v1/oidc-router.ts @@ -13,6 +13,7 @@ import { z } from "zod"; import { OidcConfigsSchema } from "@app/db/schemas"; import { OIDCConfigurationType, OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types"; +import { ApiDocsTags, OidcSSo } from "@app/lib/api-docs"; 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"; @@ -153,10 +154,18 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { config: { rateLimit: readLimit }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.OidcSso], + description: "Get OIDC config", + security: [ + { + bearerAuth: [] + } + ], querystring: z.object({ - orgSlug: z.string().trim() + organizationId: z.string().trim().describe(OidcSSo.GET_CONFIG.organizationId) }), response: { 200: SanitizedOidcConfigSchema.pick({ @@ -180,9 +189,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { orgSlug } = req.query; const oidc = await server.services.oidc.getOidc({ - orgSlug, + organizationId: req.query.organizationId, type: "external", actor: req.permission.type, actorId: req.permission.id, @@ -200,8 +208,16 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { config: { rateLimit: writeLimit }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.OidcSso], + description: "Update OIDC config", + security: [ + { + bearerAuth: [] + } + ], body: z .object({ allowedEmailDomains: z @@ -216,22 +232,26 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { .split(",") .map((id) => id.trim()) .join(", "); - }), - discoveryURL: z.string().trim(), - configurationType: z.nativeEnum(OIDCConfigurationType), - issuer: z.string().trim(), - authorizationEndpoint: z.string().trim(), - jwksUri: z.string().trim(), - tokenEndpoint: z.string().trim(), - userinfoEndpoint: z.string().trim(), - clientId: z.string().trim(), - clientSecret: z.string().trim(), - isActive: z.boolean(), - manageGroupMemberships: z.boolean().optional(), - jwtSignatureAlgorithm: z.nativeEnum(OIDCJWTSignatureAlgorithm).optional() + }) + .describe(OidcSSo.UPDATE_CONFIG.allowedEmailDomains), + discoveryURL: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.discoveryURL), + configurationType: z.nativeEnum(OIDCConfigurationType).describe(OidcSSo.UPDATE_CONFIG.configurationType), + issuer: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.issuer), + authorizationEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.authorizationEndpoint), + jwksUri: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.jwksUri), + tokenEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.tokenEndpoint), + userinfoEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.userinfoEndpoint), + clientId: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.clientId), + clientSecret: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.clientSecret), + isActive: z.boolean().describe(OidcSSo.UPDATE_CONFIG.isActive), + manageGroupMemberships: z.boolean().optional().describe(OidcSSo.UPDATE_CONFIG.manageGroupMemberships), + jwtSignatureAlgorithm: z + .nativeEnum(OIDCJWTSignatureAlgorithm) + .optional() + .describe(OidcSSo.UPDATE_CONFIG.jwtSignatureAlgorithm) }) .partial() - .merge(z.object({ orgSlug: z.string() })), + .merge(z.object({ organizationId: z.string().describe(OidcSSo.UPDATE_CONFIG.organizationId) })), response: { 200: SanitizedOidcConfigSchema.pick({ id: true, @@ -267,8 +287,16 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { config: { rateLimit: writeLimit }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.OidcSso], + description: "Create OIDC config", + security: [ + { + bearerAuth: [] + } + ], body: z .object({ allowedEmailDomains: z @@ -283,23 +311,34 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { .split(",") .map((id) => id.trim()) .join(", "); - }), - configurationType: z.nativeEnum(OIDCConfigurationType), - issuer: z.string().trim().optional().default(""), - discoveryURL: z.string().trim().optional().default(""), - authorizationEndpoint: z.string().trim().optional().default(""), - jwksUri: z.string().trim().optional().default(""), - tokenEndpoint: z.string().trim().optional().default(""), - userinfoEndpoint: z.string().trim().optional().default(""), - clientId: z.string().trim(), - clientSecret: z.string().trim(), - isActive: z.boolean(), - orgSlug: z.string().trim(), - manageGroupMemberships: z.boolean().optional().default(false), + }) + .describe(OidcSSo.CREATE_CONFIG.allowedEmailDomains), + configurationType: z.nativeEnum(OIDCConfigurationType).describe(OidcSSo.CREATE_CONFIG.configurationType), + issuer: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.issuer), + discoveryURL: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.discoveryURL), + authorizationEndpoint: z + .string() + .trim() + .optional() + .default("") + .describe(OidcSSo.CREATE_CONFIG.authorizationEndpoint), + jwksUri: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.jwksUri), + tokenEndpoint: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.tokenEndpoint), + userinfoEndpoint: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.userinfoEndpoint), + clientId: z.string().trim().describe(OidcSSo.CREATE_CONFIG.clientId), + clientSecret: z.string().trim().describe(OidcSSo.CREATE_CONFIG.clientSecret), + isActive: z.boolean().describe(OidcSSo.CREATE_CONFIG.isActive), + organizationId: z.string().trim().describe(OidcSSo.CREATE_CONFIG.organizationId), + manageGroupMemberships: z + .boolean() + .optional() + .default(false) + .describe(OidcSSo.CREATE_CONFIG.manageGroupMemberships), jwtSignatureAlgorithm: z .nativeEnum(OIDCJWTSignatureAlgorithm) .optional() .default(OIDCJWTSignatureAlgorithm.RS256) + .describe(OidcSSo.CREATE_CONFIG.jwtSignatureAlgorithm) }) .superRefine((data, ctx) => { if (data.configurationType === OIDCConfigurationType.CUSTOM) { diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index c8395d608..f8e371d01 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -13,6 +13,7 @@ import { FastifyRequest } from "fastify"; import { z } from "zod"; import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml-config-types"; +import { ApiDocsTags, SamlSso } from "@app/lib/api-docs"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; @@ -149,8 +150,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { firstName, lastName: lastName as string, relayState: (req.body as { RelayState?: string }).RelayState, - authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string, - orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string, + authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider, + orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId, metadata: userMetadata }); cb(null, { isUserCompleted, providerAuthToken }); @@ -262,25 +263,31 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { config: { rateLimit: readLimit }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.SamlSso], + description: "Get SAML config", + security: [ + { + bearerAuth: [] + } + ], querystring: z.object({ - organizationId: z.string().trim() + organizationId: z.string().trim().describe(SamlSso.GET_CONFIG.organizationId) }), response: { - 200: z - .object({ - id: z.string(), - organization: z.string(), - orgId: z.string(), - authProvider: z.string(), - isActive: z.boolean(), - entryPoint: z.string(), - issuer: z.string(), - cert: z.string(), - lastUsed: z.date().nullable().optional() - }) - .optional() + 200: z.object({ + id: z.string(), + organization: z.string(), + orgId: z.string(), + authProvider: z.string(), + isActive: z.boolean(), + entryPoint: z.string(), + issuer: z.string(), + cert: z.string(), + lastUsed: z.date().nullable().optional() + }) } }, handler: async (req) => { @@ -302,15 +309,23 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { config: { rateLimit: writeLimit }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.SamlSso], + description: "Create SAML config", + security: [ + { + bearerAuth: [] + } + ], body: z.object({ - organizationId: z.string(), - authProvider: z.nativeEnum(SamlProviders), - isActive: z.boolean(), - entryPoint: z.string(), - issuer: z.string(), - cert: z.string() + organizationId: z.string().trim().describe(SamlSso.CREATE_CONFIG.organizationId), + authProvider: z.nativeEnum(SamlProviders).describe(SamlSso.CREATE_CONFIG.authProvider), + isActive: z.boolean().describe(SamlSso.CREATE_CONFIG.isActive), + entryPoint: z.string().trim().describe(SamlSso.CREATE_CONFIG.entryPoint), + issuer: z.string().trim().describe(SamlSso.CREATE_CONFIG.issuer), + cert: z.string().trim().describe(SamlSso.CREATE_CONFIG.cert) }), response: { 200: SanitizedSamlConfigSchema @@ -341,18 +356,26 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { config: { rateLimit: writeLimit }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.SamlSso], + description: "Update SAML config", + security: [ + { + bearerAuth: [] + } + ], body: z .object({ - authProvider: z.nativeEnum(SamlProviders), - isActive: z.boolean(), - entryPoint: z.string(), - issuer: z.string(), - cert: z.string() + authProvider: z.nativeEnum(SamlProviders).describe(SamlSso.UPDATE_CONFIG.authProvider), + isActive: z.boolean().describe(SamlSso.UPDATE_CONFIG.isActive), + entryPoint: z.string().trim().describe(SamlSso.UPDATE_CONFIG.entryPoint), + issuer: z.string().trim().describe(SamlSso.UPDATE_CONFIG.issuer), + cert: z.string().trim().describe(SamlSso.UPDATE_CONFIG.cert) }) .partial() - .merge(z.object({ organizationId: z.string() })), + .merge(z.object({ organizationId: z.string().trim().describe(SamlSso.UPDATE_CONFIG.organizationId) })), response: { 200: SanitizedSamlConfigSchema } diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index a5088bde5..1a3374035 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -107,34 +107,26 @@ export const oidcConfigServiceFactory = ({ kmsService }: TOidcConfigServiceFactoryDep) => { const getOidc = async (dto: TGetOidcCfgDTO) => { - const org = await orgDAL.findOne({ slug: dto.orgSlug }); - if (!org) { + const oidcCfg = await oidcConfigDAL.findOne({ + orgId: dto.organizationId + }); + if (!oidcCfg) { throw new NotFoundError({ - message: `Organization with slug '${dto.orgSlug}' not found`, - name: "OrgNotFound" + message: `OIDC configuration for organization with ID '${dto.organizationId}' not found` }); } + if (dto.type === "external") { const { permission } = await permissionService.getOrgPermission( dto.actor, dto.actorId, - org.id, + dto.organizationId, dto.actorAuthMethod, dto.actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); } - const oidcCfg = await oidcConfigDAL.findOne({ - orgId: org.id - }); - - if (!oidcCfg) { - throw new NotFoundError({ - message: `OIDC configuration for organization with slug '${dto.orgSlug}' not found` - }); - } - const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId: oidcCfg.orgId @@ -465,7 +457,7 @@ export const oidcConfigServiceFactory = ({ }; const updateOidcCfg = async ({ - orgSlug, + organizationId, allowedEmailDomains, configurationType, discoveryURL, @@ -484,13 +476,11 @@ export const oidcConfigServiceFactory = ({ manageGroupMemberships, jwtSignatureAlgorithm }: TUpdateOidcCfgDTO) => { - const org = await orgDAL.findOne({ - slug: orgSlug - }); + const org = await orgDAL.findOne({ id: organizationId }); if (!org) { throw new NotFoundError({ - message: `Organization with slug '${orgSlug}' not found` + message: `Organization with ID '${organizationId}' not found` }); } @@ -555,7 +545,7 @@ export const oidcConfigServiceFactory = ({ }; const createOidcCfg = async ({ - orgSlug, + organizationId, allowedEmailDomains, configurationType, discoveryURL, @@ -574,12 +564,10 @@ export const oidcConfigServiceFactory = ({ manageGroupMemberships, jwtSignatureAlgorithm }: TCreateOidcCfgDTO) => { - const org = await orgDAL.findOne({ - slug: orgSlug - }); + const org = await orgDAL.findOne({ id: organizationId }); if (!org) { throw new NotFoundError({ - message: `Organization with slug '${orgSlug}' not found` + message: `Organization with ID '${organizationId}' not found` }); } @@ -639,7 +627,7 @@ export const oidcConfigServiceFactory = ({ const oidcCfg = await getOidc({ type: "internal", - orgSlug + organizationId: org.id }); if (!oidcCfg || !oidcCfg.isActive) { diff --git a/backend/src/ee/services/oidc/oidc-config-types.ts b/backend/src/ee/services/oidc/oidc-config-types.ts index c56427e63..f38f2172f 100644 --- a/backend/src/ee/services/oidc/oidc-config-types.ts +++ b/backend/src/ee/services/oidc/oidc-config-types.ts @@ -26,11 +26,11 @@ export type TOidcLoginDTO = { export type TGetOidcCfgDTO = | ({ type: "external"; - orgSlug: string; + organizationId: string; } & TGenericPermission) | { type: "internal"; - orgSlug: string; + organizationId: string; }; export type TCreateOidcCfgDTO = { @@ -45,7 +45,7 @@ export type TCreateOidcCfgDTO = { clientId: string; clientSecret: string; isActive: boolean; - orgSlug: string; + organizationId: string; manageGroupMemberships: boolean; jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm; } & TGenericPermission; @@ -62,7 +62,7 @@ export type TUpdateOidcCfgDTO = Partial<{ clientId: string; clientSecret: string; isActive: boolean; - orgSlug: string; + organizationId: string; manageGroupMemberships: boolean; jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm; }> & diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index c81fd518b..5430b0afc 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -148,10 +148,18 @@ export const samlConfigServiceFactory = ({ let samlConfig: TSamlConfigs | undefined; if (dto.type === "org") { samlConfig = await samlConfigDAL.findOne({ orgId: dto.orgId }); - if (!samlConfig) return; + if (!samlConfig) { + throw new NotFoundError({ + message: `SAML configuration for organization with ID '${dto.orgId}' not found` + }); + } } else if (dto.type === "orgSlug") { const org = await orgDAL.findOne({ slug: dto.orgSlug }); - if (!org) return; + if (!org) { + throw new NotFoundError({ + message: `Organization with slug '${dto.orgSlug}' not found` + }); + } samlConfig = await samlConfigDAL.findOne({ orgId: org.id }); } else if (dto.type === "ssoId") { // TODO: diff --git a/backend/src/ee/services/saml-config/saml-config-types.ts b/backend/src/ee/services/saml-config/saml-config-types.ts index a9bd8f485..f4ede04fa 100644 --- a/backend/src/ee/services/saml-config/saml-config-types.ts +++ b/backend/src/ee/services/saml-config/saml-config-types.ts @@ -61,20 +61,17 @@ export type TSamlLoginDTO = { export type TSamlConfigServiceFactory = { createSamlCfg: (arg: TCreateSamlCfgDTO) => Promise; updateSamlCfg: (arg: TUpdateSamlCfgDTO) => Promise; - getSaml: (arg: TGetSamlCfgDTO) => Promise< - | { - id: string; - organization: string; - orgId: string; - authProvider: string; - isActive: boolean; - entryPoint: string; - issuer: string; - cert: string; - lastUsed: Date | null | undefined; - } - | undefined - >; + getSaml: (arg: TGetSamlCfgDTO) => Promise<{ + id: string; + organization: string; + orgId: string; + authProvider: string; + isActive: boolean; + entryPoint: string; + issuer: string; + cert: string; + lastUsed: Date | null | undefined; + }>; samlLogin: (arg: TSamlLoginDTO) => Promise<{ isUserCompleted: boolean; providerAuthToken: string; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index c6fd06dac..f3d73896e 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -66,7 +66,10 @@ export enum ApiDocsTags { KmsKeys = "KMS Keys", KmsEncryption = "KMS Encryption", KmsSigning = "KMS Signing", - SecretScanning = "Secret Scanning" + SecretScanning = "Secret Scanning", + OidcSso = "OIDC SSO", + SamlSso = "SAML SSO", + LdapSso = "LDAP SSO" } export const GROUPS = { @@ -2662,3 +2665,113 @@ export const SecretScanningConfigs = { content: "The contents of the Secret Scanning Configuration file." } }; + +export const OidcSSo = { + GET_CONFIG: { + organizationId: "The ID of the organization to get the OIDC config for." + }, + UPDATE_CONFIG: { + organizationId: "The ID of the organization to update the OIDC config for.", + allowedEmailDomains: + "A list of allowed email domains that users can use to authenticate with. This field is comma separated. Example: 'example.com,acme.com'", + discoveryURL: "The URL of the OIDC discovery endpoint.", + configurationType: "The configuration type to use for the OIDC configuration.", + issuer: + "The issuer for the OIDC configuration. This is only supported when the OIDC configuration type is set to 'custom'.", + authorizationEndpoint: + "The endpoint to use for OIDC authorization. This is only supported when the OIDC configuration type is set to 'custom'.", + jwksUri: "The URL of the OIDC JWKS endpoint.", + tokenEndpoint: "The token endpoint to use for OIDC token exchange.", + userinfoEndpoint: "The userinfo endpoint to get user information from the OIDC provider.", + clientId: "The client ID to use for OIDC authentication.", + clientSecret: "The client secret to use for OIDC authentication.", + isActive: "Whether to enable or disable this OIDC configuration.", + manageGroupMemberships: + "Whether to manage group memberships for the OIDC configuration. If enabled, users will automatically be assigned groups when they sign in, based on which groups they are a member of in the OIDC provider.", + jwtSignatureAlgorithm: "The algorithm to use for JWT signature verification." + }, + CREATE_CONFIG: { + organizationId: "The ID of the organization to create the OIDC config for.", + allowedEmailDomains: + "A list of allowed email domains that users can use to authenticate with. This field is comma separated.", + discoveryURL: "The URL of the OIDC discovery endpoint.", + configurationType: "The configuration type to use for the OIDC configuration.", + issuer: + "The issuer for the OIDC configuration. This is only supported when the OIDC configuration type is set to 'custom'.", + authorizationEndpoint: + "The authorization endpoint to use for OIDC authorization. This is only supported when the OIDC configuration type is set to 'custom'.", + jwksUri: "The URL of the OIDC JWKS endpoint.", + tokenEndpoint: "The token endpoint to use for OIDC token exchange.", + userinfoEndpoint: "The userinfo endpoint to get user information from the OIDC provider.", + clientId: "The client ID to use for OIDC authentication.", + clientSecret: "The client secret to use for OIDC authentication.", + isActive: "Whether to enable or disable this OIDC configuration.", + manageGroupMemberships: + "Whether to manage group memberships for the OIDC configuration. If enabled, users will automatically be assigned groups when they sign in, based on which groups they are a member of in the OIDC provider.", + jwtSignatureAlgorithm: "The algorithm to use for JWT signature verification." + } +}; + +export const SamlSso = { + GET_CONFIG: { + organizationId: "The ID of the organization to get the SAML config for." + }, + UPDATE_CONFIG: { + organizationId: "The ID of the organization to update the SAML config for.", + authProvider: "Authentication provider to use for SAML authentication.", + isActive: "Whether to enable or disable this SAML configuration.", + entryPoint: + "The entry point for the SAML authentication. This is the URL that the user will be redirected to after they have authenticated with the SAML provider.", + issuer: "The SAML provider issuer URL or entity ID.", + cert: "The certificate to use for SAML authentication." + }, + CREATE_CONFIG: { + organizationId: "The ID of the organization to create the SAML config for.", + authProvider: "Authentication provider to use for SAML authentication.", + isActive: "Whether to enable or disable this SAML configuration.", + entryPoint: + "The entry point for the SAML authentication. This is the URL that the user will be redirected to after they have authenticated with the SAML provider.", + issuer: "The SAML provider issuer URL or entity ID.", + cert: "The certificate to use for SAML authentication." + } +}; + +export const LdapSso = { + GET_CONFIG: { + organizationId: "The ID of the organization to get the LDAP config for." + }, + CREATE_CONFIG: { + organizationId: "The ID of the organization to create the LDAP config for.", + isActive: "Whether to enable or disable this LDAP configuration.", + url: "The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` (for connection over SSL/TLS), etc.", + bindDN: + "The distinguished name of the object to bind when performing the user search such as `cn=infisical,ou=Users,dc=acme,dc=com`", + bindPass: "The password to use along with Bind DN when performing the user search.", + searchBase: "The base DN to use for the user search such as `ou=Users,dc=acme,dc=com`", + uniqueUserAttribute: + "The attribute to use as the unique identifier of LDAP users such as `sAMAccountName`, `cn`, `uid`, `objectGUID`. If left blank, defaults to uidNumber", + searchFilter: + "The template used to construct the LDAP user search filter such as `(uid={{username}})` uses literal `{{username}}` to have the given username used in the search. The default is `(uid={{username}})` which is compatible with several common directory schemas.", + groupSearchBase: "LDAP search base to use for group membership search such as `ou=Groups,dc=acme,dc=com`", + groupSearchFilter: + "The template used when constructing the group membership query such as `(&(objectClass=posixGroup)(memberUid={{.Username}}))`. The template can access the following context variables: `[UserDN, UserName]`. The default is `(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))` which is compatible with several common directory schemas.", + caCert: "The CA certificate to use when verifying the LDAP server certificate." + }, + UPDATE_CONFIG: { + organizationId: "The ID of the organization to update the LDAP config for.", + isActive: "Whether to enable or disable this LDAP configuration.", + url: "The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` (for connection over SSL/TLS), etc.", + bindDN: + "The distinguished name of object to bind when performing the user search such as `cn=infisical,ou=Users,dc=acme,dc=com`", + bindPass: "The password to use along with Bind DN when performing the user search.", + uniqueUserAttribute: + "The attribute to use as the unique identifier of LDAP users such as `sAMAccountName`, `cn`, `uid`, `objectGUID`. If left blank, defaults to uidNumber", + searchFilter: + "The template used to construct the LDAP user search filter such as `(uid={{username}})` uses literal `{{username}}` to have the given username used in the search. The default is `(uid={{username}})` which is compatible with several common directory schemas.", + searchBase: "The base DN to use for the user search such as `ou=Users,dc=acme,dc=com`", + groupSearchBase: "LDAP search base to use for group membership search such as `ou=Groups,dc=acme,dc=com`", + groupSearchFilter: + "The template used when constructing the group membership query such as `(&(objectClass=posixGroup)(memberUid={{.Username}}))`. The template can access the following context variables: `[UserDN, UserName]`. The default is `(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))` which is compatible with several common directory schemas.", + caCert: "The CA certificate to use when verifying the LDAP server certificate." + } +}; diff --git a/docs/api-reference/endpoints/organizations/ldap-sso/create-ldap-config.mdx b/docs/api-reference/endpoints/organizations/ldap-sso/create-ldap-config.mdx new file mode 100644 index 000000000..3edabd366 --- /dev/null +++ b/docs/api-reference/endpoints/organizations/ldap-sso/create-ldap-config.mdx @@ -0,0 +1,4 @@ +--- +title: "Create LDAP SSO Config" +openapi: "POST /api/v1/ldap/config" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/organizations/ldap-sso/get-ldap-config.mdx b/docs/api-reference/endpoints/organizations/ldap-sso/get-ldap-config.mdx new file mode 100644 index 000000000..a41669384 --- /dev/null +++ b/docs/api-reference/endpoints/organizations/ldap-sso/get-ldap-config.mdx @@ -0,0 +1,4 @@ +--- +title: "Get LDAP SSO Config" +openapi: "GET /api/v1/ldap/config" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/organizations/ldap-sso/update-ldap-config.mdx b/docs/api-reference/endpoints/organizations/ldap-sso/update-ldap-config.mdx new file mode 100644 index 000000000..ab613728a --- /dev/null +++ b/docs/api-reference/endpoints/organizations/ldap-sso/update-ldap-config.mdx @@ -0,0 +1,4 @@ +--- +title: "Update LDAP SSO Config" +openapi: "PATCH /api/v1/ldap/config" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/organizations/oidc-sso/create-oidc-config.mdx b/docs/api-reference/endpoints/organizations/oidc-sso/create-oidc-config.mdx new file mode 100644 index 000000000..6a86d970c --- /dev/null +++ b/docs/api-reference/endpoints/organizations/oidc-sso/create-oidc-config.mdx @@ -0,0 +1,4 @@ +--- +title: "Create OIDC Config" +openapi: "POST /api/v1/sso/oidc/config" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/organizations/oidc-sso/get-oidc-config.mdx b/docs/api-reference/endpoints/organizations/oidc-sso/get-oidc-config.mdx new file mode 100644 index 000000000..af62c7d0a --- /dev/null +++ b/docs/api-reference/endpoints/organizations/oidc-sso/get-oidc-config.mdx @@ -0,0 +1,4 @@ +--- +title: "Get OIDC Config" +openapi: "GET /api/v1/sso/oidc/config" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/organizations/oidc-sso/update-oidc-config.mdx b/docs/api-reference/endpoints/organizations/oidc-sso/update-oidc-config.mdx new file mode 100644 index 000000000..fdafeaf99 --- /dev/null +++ b/docs/api-reference/endpoints/organizations/oidc-sso/update-oidc-config.mdx @@ -0,0 +1,4 @@ +--- +title: "Update OIDC Config" +openapi: "PATCH /api/v1/sso/oidc/config" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/organizations/saml-sso/create-saml-config.mdx b/docs/api-reference/endpoints/organizations/saml-sso/create-saml-config.mdx new file mode 100644 index 000000000..0a01fb742 --- /dev/null +++ b/docs/api-reference/endpoints/organizations/saml-sso/create-saml-config.mdx @@ -0,0 +1,4 @@ +--- +title: "Create SAML SSO Config" +openapi: "POST /api/v1/sso/config" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/organizations/saml-sso/get-saml-config.mdx b/docs/api-reference/endpoints/organizations/saml-sso/get-saml-config.mdx new file mode 100644 index 000000000..71c00fb24 --- /dev/null +++ b/docs/api-reference/endpoints/organizations/saml-sso/get-saml-config.mdx @@ -0,0 +1,4 @@ +--- +title: "Get SAML SSO Config" +openapi: "GET /api/v1/sso/config" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/organizations/saml-sso/update-saml-config.mdx b/docs/api-reference/endpoints/organizations/saml-sso/update-saml-config.mdx new file mode 100644 index 000000000..57067dbd1 --- /dev/null +++ b/docs/api-reference/endpoints/organizations/saml-sso/update-saml-config.mdx @@ -0,0 +1,4 @@ +--- +title: "Update SAML SSO Config" +openapi: "PATCH /api/v1/sso/config" +--- \ No newline at end of file diff --git a/docs/docs.json b/docs/docs.json index 015ecb3c4..2aa545375 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -851,6 +851,30 @@ { "group": "Organizations", "pages": [ + { + "group": "OIDC SSO", + "pages": [ + "api-reference/endpoints/organizations/oidc-sso/get-oidc-config", + "api-reference/endpoints/organizations/oidc-sso/update-oidc-config", + "api-reference/endpoints/organizations/oidc-sso/create-oidc-config" + ] + }, + { + "group": "LDAP SSO", + "pages": [ + "api-reference/endpoints/organizations/ldap-sso/get-ldap-config", + "api-reference/endpoints/organizations/ldap-sso/update-ldap-config", + "api-reference/endpoints/organizations/ldap-sso/create-ldap-config" + ] + }, + { + "group": "SAML SSO", + "pages": [ + "api-reference/endpoints/organizations/saml-sso/get-saml-config", + "api-reference/endpoints/organizations/saml-sso/update-saml-config", + "api-reference/endpoints/organizations/saml-sso/create-saml-config" + ] + }, "api-reference/endpoints/organizations/memberships", "api-reference/endpoints/organizations/update-membership", "api-reference/endpoints/organizations/delete-membership", diff --git a/frontend/src/hooks/api/oidcConfig/mutations.tsx b/frontend/src/hooks/api/oidcConfig/mutations.tsx index 4cf4ede93..2150a17bc 100644 --- a/frontend/src/hooks/api/oidcConfig/mutations.tsx +++ b/frontend/src/hooks/api/oidcConfig/mutations.tsx @@ -21,7 +21,7 @@ export const useUpdateOIDCConfig = () => { clientId, clientSecret, isActive, - orgSlug, + organizationId, manageGroupMemberships, jwtSignatureAlgorithm }: { @@ -36,7 +36,7 @@ export const useUpdateOIDCConfig = () => { clientSecret?: string; isActive?: boolean; configurationType?: string; - orgSlug: string; + organizationId: string; manageGroupMemberships?: boolean; jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm; }) => { @@ -50,7 +50,7 @@ export const useUpdateOIDCConfig = () => { tokenEndpoint, userinfoEndpoint, clientId, - orgSlug, + organizationId, clientSecret, isActive, manageGroupMemberships, @@ -60,7 +60,7 @@ export const useUpdateOIDCConfig = () => { return data; }, onSuccess(_, dto) { - queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.orgSlug) }); + queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.organizationId) }); queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations }); } }); @@ -81,7 +81,7 @@ export const useCreateOIDCConfig = () => { clientId, clientSecret, isActive, - orgSlug, + organizationId, manageGroupMemberships, jwtSignatureAlgorithm }: { @@ -95,7 +95,7 @@ export const useCreateOIDCConfig = () => { clientId: string; clientSecret: string; isActive: boolean; - orgSlug: string; + organizationId: string; allowedEmailDomains?: string; manageGroupMemberships?: boolean; jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm; @@ -112,7 +112,7 @@ export const useCreateOIDCConfig = () => { clientId, clientSecret, isActive, - orgSlug, + organizationId, manageGroupMemberships, jwtSignatureAlgorithm }); @@ -120,7 +120,7 @@ export const useCreateOIDCConfig = () => { return data; }, onSuccess(_, dto) { - queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.orgSlug) }); + queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.organizationId) }); } }); }; diff --git a/frontend/src/hooks/api/oidcConfig/queries.tsx b/frontend/src/hooks/api/oidcConfig/queries.tsx index 38c939520..b9ee943f7 100644 --- a/frontend/src/hooks/api/oidcConfig/queries.tsx +++ b/frontend/src/hooks/api/oidcConfig/queries.tsx @@ -5,18 +5,18 @@ import { apiRequest } from "@app/config/request"; import { OIDCConfigData } from "./types"; export const oidcConfigKeys = { - getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const, + getOIDCConfig: (orgId: string) => [{ orgId }, "organization-oidc"] as const, getOIDCManageGroupMembershipsEnabled: (orgId: string) => ["oidc-manage-group-memberships", orgId] as const }; -export const useGetOIDCConfig = (orgSlug: string) => { +export const useGetOIDCConfig = (orgId: string) => { return useQuery({ - queryKey: oidcConfigKeys.getOIDCConfig(orgSlug), + queryKey: oidcConfigKeys.getOIDCConfig(orgId), queryFn: async () => { try { const { data } = await apiRequest.get( - `/api/v1/sso/oidc/config?orgSlug=${orgSlug}` + `/api/v1/sso/oidc/config?organizationId=${orgId}` ); return data; diff --git a/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OIDCModal.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OIDCModal.tsx index 52df20285..03f946ace 100644 --- a/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OIDCModal.tsx +++ b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OIDCModal.tsx @@ -105,7 +105,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele const { mutateAsync: updateMutateAsync, isPending: updateIsLoading } = useUpdateOIDCConfig(); const [isDeletePopupOpen, setIsDeletePopupOpen] = useToggle(false); - const { data } = useGetOIDCConfig(currentOrg?.slug ?? ""); + const { data } = useGetOIDCConfig(currentOrg?.id ?? ""); const { control, handleSubmit, reset, setValue, watch } = useForm({ resolver: zodResolver(schema), @@ -134,7 +134,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele clientId: "", clientSecret: "", isActive: false, - orgSlug: currentOrg.slug + organizationId: currentOrg.id }); createNotification({ @@ -196,7 +196,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele clientId, clientSecret, isActive: true, - orgSlug: currentOrg.slug, + organizationId: currentOrg.id, jwtSignatureAlgorithm }); } else { @@ -212,7 +212,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele clientId, clientSecret, isActive: true, - orgSlug: currentOrg.slug, + organizationId: currentOrg.id, jwtSignatureAlgorithm }); } diff --git a/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgOIDCSection.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgOIDCSection.tsx index 8d5021d53..3956ba894 100644 --- a/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgOIDCSection.tsx +++ b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgOIDCSection.tsx @@ -21,7 +21,7 @@ export const OrgOIDCSection = (): JSX.Element => { const { currentOrg } = useOrganization(); const { subscription } = useSubscription(); - const { data, isPending } = useGetOIDCConfig(currentOrg?.slug ?? ""); + const { data, isPending } = useGetOIDCConfig(currentOrg?.id ?? ""); const { mutateAsync } = useUpdateOIDCConfig(); const { mutateAsync: updateOrg } = useUpdateOrg(); @@ -41,7 +41,7 @@ export const OrgOIDCSection = (): JSX.Element => { } await mutateAsync({ - orgSlug: currentOrg?.slug, + organizationId: currentOrg?.id, isActive: value }); @@ -114,7 +114,7 @@ export const OrgOIDCSection = (): JSX.Element => { } await mutateAsync({ - orgSlug: currentOrg?.slug, + organizationId: currentOrg?.id, manageGroupMemberships: value }); diff --git a/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgSsoTab.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgSsoTab.tsx index ca27b7517..9b11c1302 100644 --- a/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgSsoTab.tsx +++ b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgSsoTab.tsx @@ -38,7 +38,7 @@ export const OrgSsoTab = withPermission( const { subscription } = useSubscription(); const { data: oidcConfig, isPending: isLoadingOidcConfig } = useGetOIDCConfig( - currentOrg?.slug ?? "" + currentOrg?.id ?? "" ); const { data: samlConfig, isPending: isLoadingSamlConfig } = useGetSSOConfig( currentOrg?.id ?? ""