From abfc5736fd985f516b4d449716fec2ff5fd1c973 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 1 Jul 2025 02:05:53 +0400 Subject: [PATCH 1/6] docs(api-reference/organizations): document SSO configuration endpoints --- backend/src/ee/routes/v1/ldap-router.ts | 78 ++++++++---- backend/src/ee/routes/v1/oidc-router.ts | 100 ++++++++++----- backend/src/ee/routes/v1/saml-router.ts | 57 ++++++--- backend/src/lib/api-docs/constants.ts | 115 +++++++++++++++++- .../ldap-sso/create-ldap-config.mdx | 4 + .../ldap-sso/get-ldap-config.mdx | 4 + .../ldap-sso/update-ldap-config.mdx | 4 + .../oidc-sso/create-oidc-config.mdx | 4 + .../oidc-sso/get-oidc-config.mdx | 4 + .../oidc-sso/update-oidc-config.mdx | 4 + .../saml-sso/create-saml-config.mdx | 4 + .../saml-sso/get-saml-config.mdx | 4 + .../saml-sso/update-saml-config.mdx | 4 + docs/docs.json | 28 ++++- 14 files changed, 339 insertions(+), 75 deletions(-) create mode 100644 docs/api-reference/endpoints/organizations/ldap-sso/create-ldap-config.mdx create mode 100644 docs/api-reference/endpoints/organizations/ldap-sso/get-ldap-config.mdx create mode 100644 docs/api-reference/endpoints/organizations/ldap-sso/update-ldap-config.mdx create mode 100644 docs/api-reference/endpoints/organizations/oidc-sso/create-oidc-config.mdx create mode 100644 docs/api-reference/endpoints/organizations/oidc-sso/get-oidc-config.mdx create mode 100644 docs/api-reference/endpoints/organizations/oidc-sso/update-oidc-config.mdx create mode 100644 docs/api-reference/endpoints/organizations/saml-sso/create-saml-config.mdx create mode 100644 docs/api-reference/endpoints/organizations/saml-sso/get-saml-config.mdx create mode 100644 docs/api-reference/endpoints/organizations/saml-sso/update-saml-config.mdx 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..520a94c52 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() + orgSlug: z.string().trim().describe(OidcSSo.GET_CONFIG.orgSlug) }), response: { 200: SanitizedOidcConfigSchema.pick({ @@ -200,8 +209,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,19 +233,23 @@ 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() })), @@ -267,8 +288,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 +312,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), + orgSlug: z.string().trim().describe(OidcSSo.CREATE_CONFIG.orgSlug), + 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..6f220c9cb 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"; @@ -262,10 +263,18 @@ 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 @@ -302,15 +311,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 +358,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/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 7c7a6f654..10e9e2718 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 = { @@ -2650,3 +2653,113 @@ export const SecretScanningConfigs = { content: "The contents of the Secret Scanning Configuration file." } }; + +export const OidcSSo = { + GET_CONFIG: { + orgSlug: "The slug of the organization to get the OIDC config for." + }, + UPDATE_CONFIG: { + orgSlug: "The slug 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.", + 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 the OIDC configuration is active.", + manageGroupMemberships: + "Whether to manage group memberships for the OIDC configuration. If enabled, users will automatically be assigned groups when they sign in or are added to a group in the OIDC provider.", + jwtSignatureAlgorithm: "The algorithm to use for JWT signature verification." + }, + CREATE_CONFIG: { + orgSlug: "The slug 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 the OIDC configuration is active.", + manageGroupMemberships: + "Whether to manage group memberships for the OIDC configuration. If enabled, users will automatically be assigned groups when they sign in or are added to a group 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: "The authentication provider to use for SAML authentication.", + isActive: "Whether the SAML configuration is active.", + 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: "The authentication provider to use for SAML authentication.", + isActive: "Whether the SAML configuration is active.", + 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 the LDAP configuration is active.", + 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.", + 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: + "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: + " 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 the LDAP configuration is active.", + 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: + "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: + " 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 ac9d62a7c..404ae79a4 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -849,6 +849,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", @@ -2089,9 +2113,9 @@ "href": "https://infisical.com" }, "api": { - "openapi": "https://app.infisical.com/api/docs/json", + "openapi": "https://db056ef4a0a9.ngrok.app/api/docs/json", "mdx": { - "server": ["https://app.infisical.com", "http://localhost:8080"] + "server": ["http://localhost:8080"] } }, "appearance": { From 13d2cbd8b0b2d463ffa2aa00fac081ceac4afb12 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 1 Jul 2025 02:09:14 +0400 Subject: [PATCH 2/6] Update docs.json --- docs/docs.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs.json b/docs/docs.json index 404ae79a4..8d90a1a58 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -2113,9 +2113,9 @@ "href": "https://infisical.com" }, "api": { - "openapi": "https://db056ef4a0a9.ngrok.app/api/docs/json", + "openapi": "https://app.infisical.com/api/docs/json", "mdx": { - "server": ["http://localhost:8080"] + "server": ["https://app.infisical.com", "http://localhost:8080"] } }, "appearance": { From cda8579ca4238fea5447d5d40d52d0d24e29e497 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 4 Jul 2025 04:51:14 +0400 Subject: [PATCH 3/6] fix: requested changes --- backend/src/ee/routes/v1/oidc-router.ts | 9 ++--- .../ee/services/oidc/oidc-config-service.ts | 40 +++++++------------ .../src/ee/services/oidc/oidc-config-types.ts | 8 ++-- backend/src/lib/api-docs/constants.ts | 36 ++++++++--------- .../src/hooks/api/oidcConfig/mutations.tsx | 16 ++++---- frontend/src/hooks/api/oidcConfig/queries.tsx | 8 ++-- .../components/OrgSsoTab/OIDCModal.tsx | 8 ++-- .../components/OrgSsoTab/OrgOIDCSection.tsx | 6 +-- .../components/OrgSsoTab/OrgSsoTab.tsx | 2 +- 9 files changed, 60 insertions(+), 73 deletions(-) diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts index 520a94c52..59c05272d 100644 --- a/backend/src/ee/routes/v1/oidc-router.ts +++ b/backend/src/ee/routes/v1/oidc-router.ts @@ -165,7 +165,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { } ], querystring: z.object({ - orgSlug: z.string().trim().describe(OidcSSo.GET_CONFIG.orgSlug) + organizationId: z.string().trim().describe(OidcSSo.GET_CONFIG.organizationId) }), response: { 200: SanitizedOidcConfigSchema.pick({ @@ -189,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, @@ -252,7 +251,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { .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, @@ -329,7 +328,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { 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), - orgSlug: z.string().trim().describe(OidcSSo.CREATE_CONFIG.orgSlug), + organizationId: z.string().trim().describe(OidcSSo.CREATE_CONFIG.organizationId), manageGroupMemberships: z .boolean() .optional() 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/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 10e9e2718..848b0d095 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2656,30 +2656,30 @@ export const SecretScanningConfigs = { export const OidcSSo = { GET_CONFIG: { - orgSlug: "The slug of the organization to get the OIDC config for." + organizationId: "The ID of the organization to get the OIDC config for." }, UPDATE_CONFIG: { - orgSlug: "The slug of the organization to update the OIDC config for.", + 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.", + "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 authorization endpoint to use for OIDC authorization. This is only supported when the OIDC configuration type is set to 'custom'.", + "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 the OIDC configuration is active.", + 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 or are added to a group in the OIDC provider.", + "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: { - orgSlug: "The slug of the organization to create the OIDC config for.", + 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.", @@ -2693,9 +2693,9 @@ export const OidcSSo = { 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 the OIDC configuration is active.", + 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 or are added to a group in the OIDC provider.", + "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." } }; @@ -2707,7 +2707,7 @@ export const SamlSso = { UPDATE_CONFIG: { organizationId: "The ID of the organization to update the SAML config for.", authProvider: "The authentication provider to use for SAML authentication.", - isActive: "Whether the SAML configuration is active.", + 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.", @@ -2716,7 +2716,7 @@ export const SamlSso = { CREATE_CONFIG: { organizationId: "The ID of the organization to create the SAML config for.", authProvider: "The authentication provider to use for SAML authentication.", - isActive: "Whether the SAML configuration is active.", + 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.", @@ -2730,24 +2730,24 @@ export const LdapSso = { }, CREATE_CONFIG: { organizationId: "The ID of the organization to create the LDAP config for.", - isActive: "Whether the LDAP configuration is active.", + 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`", + "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: - "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.", + "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: - " 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.", + "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 the LDAP configuration is active.", + 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`", @@ -2755,11 +2755,11 @@ export const LdapSso = { 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: - "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.", + "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: - " 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.", + "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/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 ?? "" From 65b1354ef15643972969151a003f2913c4d722f5 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 4 Jul 2025 05:07:54 +0400 Subject: [PATCH 4/6] fix: remove undefined return type from get saml endpoint --- backend/src/ee/routes/v1/saml-router.ts | 24 ++++++++---------- .../saml-config/saml-config-service.ts | 12 +++++++-- .../services/saml-config/saml-config-types.ts | 25 ++++++++----------- backend/src/lib/api-docs/constants.ts | 4 +-- 4 files changed, 34 insertions(+), 31 deletions(-) diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index 6f220c9cb..461e6a489 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -277,19 +277,17 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { 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) => { 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..2dee8afca 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: `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 848b0d095..d22f26624 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2706,7 +2706,7 @@ export const SamlSso = { }, UPDATE_CONFIG: { organizationId: "The ID of the organization to update the SAML config for.", - authProvider: "The authentication provider to use for SAML authentication.", + 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.", @@ -2715,7 +2715,7 @@ export const SamlSso = { }, CREATE_CONFIG: { organizationId: "The ID of the organization to create the SAML config for.", - authProvider: "The authentication provider to use for SAML authentication.", + 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.", From c6f8915d3faa82f631d0030e09099bedb2148f7b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 4 Jul 2025 05:21:54 +0400 Subject: [PATCH 5/6] Update saml-config-service.ts --- backend/src/ee/services/saml-config/saml-config-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2dee8afca..5430b0afc 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -150,7 +150,7 @@ export const samlConfigServiceFactory = ({ samlConfig = await samlConfigDAL.findOne({ orgId: dto.orgId }); if (!samlConfig) { throw new NotFoundError({ - message: `Organization with ID '${dto.orgId}' not found` + message: `SAML configuration for organization with ID '${dto.orgId}' not found` }); } } else if (dto.type === "orgSlug") { From f903e5b3d4fc8dfe5ab6034f69be4e8ab5dee3e9 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 4 Jul 2025 05:23:05 +0400 Subject: [PATCH 6/6] Update saml-router.ts --- backend/src/ee/routes/v1/saml-router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index 461e6a489..f8e371d01 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -150,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 });