Compare commits

...

6 Commits

23 changed files with 411 additions and 157 deletions

View File

@ -17,6 +17,7 @@ import { z } from "zod";
import { LdapGroupMapsSchema } from "@app/db/schemas"; import { LdapGroupMapsSchema } from "@app/db/schemas";
import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types"; import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types";
import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns"; 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 { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@ -132,10 +133,18 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.LdapSso],
description: "Get LDAP config",
security: [
{
bearerAuth: []
}
],
querystring: z.object({ querystring: z.object({
organizationId: z.string().trim() organizationId: z.string().trim().describe(LdapSso.GET_CONFIG.organizationId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@ -172,23 +181,32 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.LdapSso],
description: "Create LDAP config",
security: [
{
bearerAuth: []
}
],
body: z.object({ body: z.object({
organizationId: z.string().trim(), organizationId: z.string().trim().describe(LdapSso.CREATE_CONFIG.organizationId),
isActive: z.boolean(), isActive: z.boolean().describe(LdapSso.CREATE_CONFIG.isActive),
url: z.string().trim(), url: z.string().trim().describe(LdapSso.CREATE_CONFIG.url),
bindDN: z.string().trim(), bindDN: z.string().trim().describe(LdapSso.CREATE_CONFIG.bindDN),
bindPass: z.string().trim(), bindPass: z.string().trim().describe(LdapSso.CREATE_CONFIG.bindPass),
uniqueUserAttribute: z.string().trim().default("uidNumber"), uniqueUserAttribute: z.string().trim().default("uidNumber").describe(LdapSso.CREATE_CONFIG.uniqueUserAttribute),
searchBase: z.string().trim(), searchBase: z.string().trim().describe(LdapSso.CREATE_CONFIG.searchBase),
searchFilter: z.string().trim().default("(uid={{username}})"), searchFilter: z.string().trim().default("(uid={{username}})").describe(LdapSso.CREATE_CONFIG.searchFilter),
groupSearchBase: z.string().trim(), groupSearchBase: z.string().trim().describe(LdapSso.CREATE_CONFIG.groupSearchBase),
groupSearchFilter: z groupSearchFilter: z
.string() .string()
.trim() .trim()
.default("(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))"), .default("(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))")
caCert: z.string().trim().default("") .describe(LdapSso.CREATE_CONFIG.groupSearchFilter),
caCert: z.string().trim().default("").describe(LdapSso.CREATE_CONFIG.caCert)
}), }),
response: { response: {
200: SanitizedLdapConfigSchema 200: SanitizedLdapConfigSchema
@ -214,23 +232,31 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.LdapSso],
description: "Update LDAP config",
security: [
{
bearerAuth: []
}
],
body: z body: z
.object({ .object({
isActive: z.boolean(), isActive: z.boolean().describe(LdapSso.UPDATE_CONFIG.isActive),
url: z.string().trim(), url: z.string().trim().describe(LdapSso.UPDATE_CONFIG.url),
bindDN: z.string().trim(), bindDN: z.string().trim().describe(LdapSso.UPDATE_CONFIG.bindDN),
bindPass: z.string().trim(), bindPass: z.string().trim().describe(LdapSso.UPDATE_CONFIG.bindPass),
uniqueUserAttribute: z.string().trim(), uniqueUserAttribute: z.string().trim().describe(LdapSso.UPDATE_CONFIG.uniqueUserAttribute),
searchBase: z.string().trim(), searchBase: z.string().trim().describe(LdapSso.UPDATE_CONFIG.searchBase),
searchFilter: z.string().trim(), searchFilter: z.string().trim().describe(LdapSso.UPDATE_CONFIG.searchFilter),
groupSearchBase: z.string().trim(), groupSearchBase: z.string().trim().describe(LdapSso.UPDATE_CONFIG.groupSearchBase),
groupSearchFilter: z.string().trim(), groupSearchFilter: z.string().trim().describe(LdapSso.UPDATE_CONFIG.groupSearchFilter),
caCert: z.string().trim() caCert: z.string().trim().describe(LdapSso.UPDATE_CONFIG.caCert)
}) })
.partial() .partial()
.merge(z.object({ organizationId: z.string() })), .merge(z.object({ organizationId: z.string().trim().describe(LdapSso.UPDATE_CONFIG.organizationId) })),
response: { response: {
200: SanitizedLdapConfigSchema 200: SanitizedLdapConfigSchema
} }

View File

@ -13,6 +13,7 @@ import { z } from "zod";
import { OidcConfigsSchema } from "@app/db/schemas"; import { OidcConfigsSchema } from "@app/db/schemas";
import { OIDCConfigurationType, OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types"; 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 { getConfig } from "@app/lib/config/env";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -153,10 +154,18 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.OidcSso],
description: "Get OIDC config",
security: [
{
bearerAuth: []
}
],
querystring: z.object({ querystring: z.object({
orgSlug: z.string().trim() organizationId: z.string().trim().describe(OidcSSo.GET_CONFIG.organizationId)
}), }),
response: { response: {
200: SanitizedOidcConfigSchema.pick({ 200: SanitizedOidcConfigSchema.pick({
@ -180,9 +189,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req) => { handler: async (req) => {
const { orgSlug } = req.query;
const oidc = await server.services.oidc.getOidc({ const oidc = await server.services.oidc.getOidc({
orgSlug, organizationId: req.query.organizationId,
type: "external", type: "external",
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@ -200,8 +208,16 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.OidcSso],
description: "Update OIDC config",
security: [
{
bearerAuth: []
}
],
body: z body: z
.object({ .object({
allowedEmailDomains: z allowedEmailDomains: z
@ -216,22 +232,26 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
.split(",") .split(",")
.map((id) => id.trim()) .map((id) => id.trim())
.join(", "); .join(", ");
}), })
discoveryURL: z.string().trim(), .describe(OidcSSo.UPDATE_CONFIG.allowedEmailDomains),
configurationType: z.nativeEnum(OIDCConfigurationType), discoveryURL: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.discoveryURL),
issuer: z.string().trim(), configurationType: z.nativeEnum(OIDCConfigurationType).describe(OidcSSo.UPDATE_CONFIG.configurationType),
authorizationEndpoint: z.string().trim(), issuer: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.issuer),
jwksUri: z.string().trim(), authorizationEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.authorizationEndpoint),
tokenEndpoint: z.string().trim(), jwksUri: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.jwksUri),
userinfoEndpoint: z.string().trim(), tokenEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.tokenEndpoint),
clientId: z.string().trim(), userinfoEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.userinfoEndpoint),
clientSecret: z.string().trim(), clientId: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.clientId),
isActive: z.boolean(), clientSecret: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.clientSecret),
manageGroupMemberships: z.boolean().optional(), isActive: z.boolean().describe(OidcSSo.UPDATE_CONFIG.isActive),
jwtSignatureAlgorithm: z.nativeEnum(OIDCJWTSignatureAlgorithm).optional() manageGroupMemberships: z.boolean().optional().describe(OidcSSo.UPDATE_CONFIG.manageGroupMemberships),
jwtSignatureAlgorithm: z
.nativeEnum(OIDCJWTSignatureAlgorithm)
.optional()
.describe(OidcSSo.UPDATE_CONFIG.jwtSignatureAlgorithm)
}) })
.partial() .partial()
.merge(z.object({ orgSlug: z.string() })), .merge(z.object({ organizationId: z.string().describe(OidcSSo.UPDATE_CONFIG.organizationId) })),
response: { response: {
200: SanitizedOidcConfigSchema.pick({ 200: SanitizedOidcConfigSchema.pick({
id: true, id: true,
@ -267,8 +287,16 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.OidcSso],
description: "Create OIDC config",
security: [
{
bearerAuth: []
}
],
body: z body: z
.object({ .object({
allowedEmailDomains: z allowedEmailDomains: z
@ -283,23 +311,34 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
.split(",") .split(",")
.map((id) => id.trim()) .map((id) => id.trim())
.join(", "); .join(", ");
}), })
configurationType: z.nativeEnum(OIDCConfigurationType), .describe(OidcSSo.CREATE_CONFIG.allowedEmailDomains),
issuer: z.string().trim().optional().default(""), configurationType: z.nativeEnum(OIDCConfigurationType).describe(OidcSSo.CREATE_CONFIG.configurationType),
discoveryURL: z.string().trim().optional().default(""), issuer: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.issuer),
authorizationEndpoint: z.string().trim().optional().default(""), discoveryURL: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.discoveryURL),
jwksUri: z.string().trim().optional().default(""), authorizationEndpoint: z
tokenEndpoint: z.string().trim().optional().default(""), .string()
userinfoEndpoint: z.string().trim().optional().default(""), .trim()
clientId: z.string().trim(), .optional()
clientSecret: z.string().trim(), .default("")
isActive: z.boolean(), .describe(OidcSSo.CREATE_CONFIG.authorizationEndpoint),
orgSlug: z.string().trim(), jwksUri: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.jwksUri),
manageGroupMemberships: z.boolean().optional().default(false), 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 jwtSignatureAlgorithm: z
.nativeEnum(OIDCJWTSignatureAlgorithm) .nativeEnum(OIDCJWTSignatureAlgorithm)
.optional() .optional()
.default(OIDCJWTSignatureAlgorithm.RS256) .default(OIDCJWTSignatureAlgorithm.RS256)
.describe(OidcSSo.CREATE_CONFIG.jwtSignatureAlgorithm)
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.configurationType === OIDCConfigurationType.CUSTOM) { if (data.configurationType === OIDCConfigurationType.CUSTOM) {

View File

@ -13,6 +13,7 @@ import { FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml-config-types"; 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 { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@ -149,8 +150,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
firstName, firstName,
lastName: lastName as string, lastName: lastName as string,
relayState: (req.body as { RelayState?: string }).RelayState, relayState: (req.body as { RelayState?: string }).RelayState,
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string, authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider,
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string, orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId,
metadata: userMetadata metadata: userMetadata
}); });
cb(null, { isUserCompleted, providerAuthToken }); cb(null, { isUserCompleted, providerAuthToken });
@ -262,25 +263,31 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SamlSso],
description: "Get SAML config",
security: [
{
bearerAuth: []
}
],
querystring: z.object({ querystring: z.object({
organizationId: z.string().trim() organizationId: z.string().trim().describe(SamlSso.GET_CONFIG.organizationId)
}), }),
response: { response: {
200: z 200: z.object({
.object({ id: z.string(),
id: z.string(), organization: z.string(),
organization: z.string(), orgId: z.string(),
orgId: z.string(), authProvider: z.string(),
authProvider: z.string(), isActive: z.boolean(),
isActive: z.boolean(), entryPoint: z.string(),
entryPoint: z.string(), issuer: z.string(),
issuer: z.string(), cert: z.string(),
cert: z.string(), lastUsed: z.date().nullable().optional()
lastUsed: z.date().nullable().optional() })
})
.optional()
} }
}, },
handler: async (req) => { handler: async (req) => {
@ -302,15 +309,23 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SamlSso],
description: "Create SAML config",
security: [
{
bearerAuth: []
}
],
body: z.object({ body: z.object({
organizationId: z.string(), organizationId: z.string().trim().describe(SamlSso.CREATE_CONFIG.organizationId),
authProvider: z.nativeEnum(SamlProviders), authProvider: z.nativeEnum(SamlProviders).describe(SamlSso.CREATE_CONFIG.authProvider),
isActive: z.boolean(), isActive: z.boolean().describe(SamlSso.CREATE_CONFIG.isActive),
entryPoint: z.string(), entryPoint: z.string().trim().describe(SamlSso.CREATE_CONFIG.entryPoint),
issuer: z.string(), issuer: z.string().trim().describe(SamlSso.CREATE_CONFIG.issuer),
cert: z.string() cert: z.string().trim().describe(SamlSso.CREATE_CONFIG.cert)
}), }),
response: { response: {
200: SanitizedSamlConfigSchema 200: SanitizedSamlConfigSchema
@ -341,18 +356,26 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SamlSso],
description: "Update SAML config",
security: [
{
bearerAuth: []
}
],
body: z body: z
.object({ .object({
authProvider: z.nativeEnum(SamlProviders), authProvider: z.nativeEnum(SamlProviders).describe(SamlSso.UPDATE_CONFIG.authProvider),
isActive: z.boolean(), isActive: z.boolean().describe(SamlSso.UPDATE_CONFIG.isActive),
entryPoint: z.string(), entryPoint: z.string().trim().describe(SamlSso.UPDATE_CONFIG.entryPoint),
issuer: z.string(), issuer: z.string().trim().describe(SamlSso.UPDATE_CONFIG.issuer),
cert: z.string() cert: z.string().trim().describe(SamlSso.UPDATE_CONFIG.cert)
}) })
.partial() .partial()
.merge(z.object({ organizationId: z.string() })), .merge(z.object({ organizationId: z.string().trim().describe(SamlSso.UPDATE_CONFIG.organizationId) })),
response: { response: {
200: SanitizedSamlConfigSchema 200: SanitizedSamlConfigSchema
} }

View File

@ -107,34 +107,26 @@ export const oidcConfigServiceFactory = ({
kmsService kmsService
}: TOidcConfigServiceFactoryDep) => { }: TOidcConfigServiceFactoryDep) => {
const getOidc = async (dto: TGetOidcCfgDTO) => { const getOidc = async (dto: TGetOidcCfgDTO) => {
const org = await orgDAL.findOne({ slug: dto.orgSlug }); const oidcCfg = await oidcConfigDAL.findOne({
if (!org) { orgId: dto.organizationId
});
if (!oidcCfg) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with slug '${dto.orgSlug}' not found`, message: `OIDC configuration for organization with ID '${dto.organizationId}' not found`
name: "OrgNotFound"
}); });
} }
if (dto.type === "external") { if (dto.type === "external") {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
dto.actor, dto.actor,
dto.actorId, dto.actorId,
org.id, dto.organizationId,
dto.actorAuthMethod, dto.actorAuthMethod,
dto.actorOrgId dto.actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); 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({ const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: oidcCfg.orgId orgId: oidcCfg.orgId
@ -465,7 +457,7 @@ export const oidcConfigServiceFactory = ({
}; };
const updateOidcCfg = async ({ const updateOidcCfg = async ({
orgSlug, organizationId,
allowedEmailDomains, allowedEmailDomains,
configurationType, configurationType,
discoveryURL, discoveryURL,
@ -484,13 +476,11 @@ export const oidcConfigServiceFactory = ({
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}: TUpdateOidcCfgDTO) => { }: TUpdateOidcCfgDTO) => {
const org = await orgDAL.findOne({ const org = await orgDAL.findOne({ id: organizationId });
slug: orgSlug
});
if (!org) { if (!org) {
throw new NotFoundError({ 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 ({ const createOidcCfg = async ({
orgSlug, organizationId,
allowedEmailDomains, allowedEmailDomains,
configurationType, configurationType,
discoveryURL, discoveryURL,
@ -574,12 +564,10 @@ export const oidcConfigServiceFactory = ({
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}: TCreateOidcCfgDTO) => { }: TCreateOidcCfgDTO) => {
const org = await orgDAL.findOne({ const org = await orgDAL.findOne({ id: organizationId });
slug: orgSlug
});
if (!org) { if (!org) {
throw new NotFoundError({ 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({ const oidcCfg = await getOidc({
type: "internal", type: "internal",
orgSlug organizationId: org.id
}); });
if (!oidcCfg || !oidcCfg.isActive) { if (!oidcCfg || !oidcCfg.isActive) {

View File

@ -26,11 +26,11 @@ export type TOidcLoginDTO = {
export type TGetOidcCfgDTO = export type TGetOidcCfgDTO =
| ({ | ({
type: "external"; type: "external";
orgSlug: string; organizationId: string;
} & TGenericPermission) } & TGenericPermission)
| { | {
type: "internal"; type: "internal";
orgSlug: string; organizationId: string;
}; };
export type TCreateOidcCfgDTO = { export type TCreateOidcCfgDTO = {
@ -45,7 +45,7 @@ export type TCreateOidcCfgDTO = {
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
isActive: boolean; isActive: boolean;
orgSlug: string; organizationId: string;
manageGroupMemberships: boolean; manageGroupMemberships: boolean;
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm; jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
} & TGenericPermission; } & TGenericPermission;
@ -62,7 +62,7 @@ export type TUpdateOidcCfgDTO = Partial<{
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
isActive: boolean; isActive: boolean;
orgSlug: string; organizationId: string;
manageGroupMemberships: boolean; manageGroupMemberships: boolean;
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm; jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
}> & }> &

View File

@ -148,10 +148,18 @@ export const samlConfigServiceFactory = ({
let samlConfig: TSamlConfigs | undefined; let samlConfig: TSamlConfigs | undefined;
if (dto.type === "org") { if (dto.type === "org") {
samlConfig = await samlConfigDAL.findOne({ orgId: dto.orgId }); 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") { } else if (dto.type === "orgSlug") {
const org = await orgDAL.findOne({ slug: dto.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 }); samlConfig = await samlConfigDAL.findOne({ orgId: org.id });
} else if (dto.type === "ssoId") { } else if (dto.type === "ssoId") {
// TODO: // TODO:

View File

@ -61,20 +61,17 @@ export type TSamlLoginDTO = {
export type TSamlConfigServiceFactory = { export type TSamlConfigServiceFactory = {
createSamlCfg: (arg: TCreateSamlCfgDTO) => Promise<TSamlConfigs>; createSamlCfg: (arg: TCreateSamlCfgDTO) => Promise<TSamlConfigs>;
updateSamlCfg: (arg: TUpdateSamlCfgDTO) => Promise<TSamlConfigs>; updateSamlCfg: (arg: TUpdateSamlCfgDTO) => Promise<TSamlConfigs>;
getSaml: (arg: TGetSamlCfgDTO) => Promise< getSaml: (arg: TGetSamlCfgDTO) => Promise<{
| { id: string;
id: string; organization: string;
organization: string; orgId: string;
orgId: string; authProvider: string;
authProvider: string; isActive: boolean;
isActive: boolean; entryPoint: string;
entryPoint: string; issuer: string;
issuer: string; cert: string;
cert: string; lastUsed: Date | null | undefined;
lastUsed: Date | null | undefined; }>;
}
| undefined
>;
samlLogin: (arg: TSamlLoginDTO) => Promise<{ samlLogin: (arg: TSamlLoginDTO) => Promise<{
isUserCompleted: boolean; isUserCompleted: boolean;
providerAuthToken: string; providerAuthToken: string;

View File

@ -66,7 +66,10 @@ export enum ApiDocsTags {
KmsKeys = "KMS Keys", KmsKeys = "KMS Keys",
KmsEncryption = "KMS Encryption", KmsEncryption = "KMS Encryption",
KmsSigning = "KMS Signing", KmsSigning = "KMS Signing",
SecretScanning = "Secret Scanning" SecretScanning = "Secret Scanning",
OidcSso = "OIDC SSO",
SamlSso = "SAML SSO",
LdapSso = "LDAP SSO"
} }
export const GROUPS = { export const GROUPS = {
@ -2650,3 +2653,113 @@ export const SecretScanningConfigs = {
content: "The contents of the Secret Scanning Configuration file." 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."
}
};

View File

@ -0,0 +1,4 @@
---
title: "Create LDAP SSO Config"
openapi: "POST /api/v1/ldap/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Get LDAP SSO Config"
openapi: "GET /api/v1/ldap/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Update LDAP SSO Config"
openapi: "PATCH /api/v1/ldap/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Create OIDC Config"
openapi: "POST /api/v1/sso/oidc/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Get OIDC Config"
openapi: "GET /api/v1/sso/oidc/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Update OIDC Config"
openapi: "PATCH /api/v1/sso/oidc/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Create SAML SSO Config"
openapi: "POST /api/v1/sso/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Get SAML SSO Config"
openapi: "GET /api/v1/sso/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Update SAML SSO Config"
openapi: "PATCH /api/v1/sso/config"
---

View File

@ -849,6 +849,30 @@
{ {
"group": "Organizations", "group": "Organizations",
"pages": [ "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/memberships",
"api-reference/endpoints/organizations/update-membership", "api-reference/endpoints/organizations/update-membership",
"api-reference/endpoints/organizations/delete-membership", "api-reference/endpoints/organizations/delete-membership",

View File

@ -21,7 +21,7 @@ export const useUpdateOIDCConfig = () => {
clientId, clientId,
clientSecret, clientSecret,
isActive, isActive,
orgSlug, organizationId,
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}: { }: {
@ -36,7 +36,7 @@ export const useUpdateOIDCConfig = () => {
clientSecret?: string; clientSecret?: string;
isActive?: boolean; isActive?: boolean;
configurationType?: string; configurationType?: string;
orgSlug: string; organizationId: string;
manageGroupMemberships?: boolean; manageGroupMemberships?: boolean;
jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm; jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm;
}) => { }) => {
@ -50,7 +50,7 @@ export const useUpdateOIDCConfig = () => {
tokenEndpoint, tokenEndpoint,
userinfoEndpoint, userinfoEndpoint,
clientId, clientId,
orgSlug, organizationId,
clientSecret, clientSecret,
isActive, isActive,
manageGroupMemberships, manageGroupMemberships,
@ -60,7 +60,7 @@ export const useUpdateOIDCConfig = () => {
return data; return data;
}, },
onSuccess(_, dto) { onSuccess(_, dto) {
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.orgSlug) }); queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.organizationId) });
queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations }); queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations });
} }
}); });
@ -81,7 +81,7 @@ export const useCreateOIDCConfig = () => {
clientId, clientId,
clientSecret, clientSecret,
isActive, isActive,
orgSlug, organizationId,
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}: { }: {
@ -95,7 +95,7 @@ export const useCreateOIDCConfig = () => {
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
isActive: boolean; isActive: boolean;
orgSlug: string; organizationId: string;
allowedEmailDomains?: string; allowedEmailDomains?: string;
manageGroupMemberships?: boolean; manageGroupMemberships?: boolean;
jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm; jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm;
@ -112,7 +112,7 @@ export const useCreateOIDCConfig = () => {
clientId, clientId,
clientSecret, clientSecret,
isActive, isActive,
orgSlug, organizationId,
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}); });
@ -120,7 +120,7 @@ export const useCreateOIDCConfig = () => {
return data; return data;
}, },
onSuccess(_, dto) { onSuccess(_, dto) {
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.orgSlug) }); queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.organizationId) });
} }
}); });
}; };

View File

@ -5,18 +5,18 @@ import { apiRequest } from "@app/config/request";
import { OIDCConfigData } from "./types"; import { OIDCConfigData } from "./types";
export const oidcConfigKeys = { export const oidcConfigKeys = {
getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const, getOIDCConfig: (orgId: string) => [{ orgId }, "organization-oidc"] as const,
getOIDCManageGroupMembershipsEnabled: (orgId: string) => getOIDCManageGroupMembershipsEnabled: (orgId: string) =>
["oidc-manage-group-memberships", orgId] as const ["oidc-manage-group-memberships", orgId] as const
}; };
export const useGetOIDCConfig = (orgSlug: string) => { export const useGetOIDCConfig = (orgId: string) => {
return useQuery({ return useQuery({
queryKey: oidcConfigKeys.getOIDCConfig(orgSlug), queryKey: oidcConfigKeys.getOIDCConfig(orgId),
queryFn: async () => { queryFn: async () => {
try { try {
const { data } = await apiRequest.get<OIDCConfigData>( const { data } = await apiRequest.get<OIDCConfigData>(
`/api/v1/sso/oidc/config?orgSlug=${orgSlug}` `/api/v1/sso/oidc/config?organizationId=${orgId}`
); );
return data; return data;

View File

@ -105,7 +105,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
const { mutateAsync: updateMutateAsync, isPending: updateIsLoading } = useUpdateOIDCConfig(); const { mutateAsync: updateMutateAsync, isPending: updateIsLoading } = useUpdateOIDCConfig();
const [isDeletePopupOpen, setIsDeletePopupOpen] = useToggle(false); const [isDeletePopupOpen, setIsDeletePopupOpen] = useToggle(false);
const { data } = useGetOIDCConfig(currentOrg?.slug ?? ""); const { data } = useGetOIDCConfig(currentOrg?.id ?? "");
const { control, handleSubmit, reset, setValue, watch } = useForm<OIDCFormData>({ const { control, handleSubmit, reset, setValue, watch } = useForm<OIDCFormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
@ -134,7 +134,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
clientId: "", clientId: "",
clientSecret: "", clientSecret: "",
isActive: false, isActive: false,
orgSlug: currentOrg.slug organizationId: currentOrg.id
}); });
createNotification({ createNotification({
@ -196,7 +196,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
clientId, clientId,
clientSecret, clientSecret,
isActive: true, isActive: true,
orgSlug: currentOrg.slug, organizationId: currentOrg.id,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}); });
} else { } else {
@ -212,7 +212,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
clientId, clientId,
clientSecret, clientSecret,
isActive: true, isActive: true,
orgSlug: currentOrg.slug, organizationId: currentOrg.id,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}); });
} }

View File

@ -21,7 +21,7 @@ export const OrgOIDCSection = (): JSX.Element => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { data, isPending } = useGetOIDCConfig(currentOrg?.slug ?? ""); const { data, isPending } = useGetOIDCConfig(currentOrg?.id ?? "");
const { mutateAsync } = useUpdateOIDCConfig(); const { mutateAsync } = useUpdateOIDCConfig();
const { mutateAsync: updateOrg } = useUpdateOrg(); const { mutateAsync: updateOrg } = useUpdateOrg();
@ -41,7 +41,7 @@ export const OrgOIDCSection = (): JSX.Element => {
} }
await mutateAsync({ await mutateAsync({
orgSlug: currentOrg?.slug, organizationId: currentOrg?.id,
isActive: value isActive: value
}); });
@ -114,7 +114,7 @@ export const OrgOIDCSection = (): JSX.Element => {
} }
await mutateAsync({ await mutateAsync({
orgSlug: currentOrg?.slug, organizationId: currentOrg?.id,
manageGroupMemberships: value manageGroupMemberships: value
}); });

View File

@ -38,7 +38,7 @@ export const OrgSsoTab = withPermission(
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { data: oidcConfig, isPending: isLoadingOidcConfig } = useGetOIDCConfig( const { data: oidcConfig, isPending: isLoadingOidcConfig } = useGetOIDCConfig(
currentOrg?.slug ?? "" currentOrg?.id ?? ""
); );
const { data: samlConfig, isPending: isLoadingSamlConfig } = useGetSSOConfig( const { data: samlConfig, isPending: isLoadingSamlConfig } = useGetSSOConfig(
currentOrg?.id ?? "" currentOrg?.id ?? ""