diff --git a/Makefile b/Makefile index 4aca41989..2352e5134 100644 --- a/Makefile +++ b/Makefile @@ -30,3 +30,6 @@ reviewable-api: npm run type:check reviewable: reviewable-ui reviewable-api + +up-dev-sso: + docker compose -f docker-compose.dev.yml --profile sso up --build diff --git a/backend/src/db/migrations/20250129214629_oidc-configs-manage-group-memberships-col.ts b/backend/src/db/migrations/20250129214629_oidc-configs-manage-group-memberships-col.ts new file mode 100644 index 000000000..74b866b77 --- /dev/null +++ b/backend/src/db/migrations/20250129214629_oidc-configs-manage-group-memberships-col.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasManageGroupMembershipsCol = await knex.schema.hasColumn(TableName.OidcConfig, "manageGroupMemberships"); + + await knex.schema.alterTable(TableName.OidcConfig, (tb) => { + if (!hasManageGroupMembershipsCol) { + tb.boolean("manageGroupMemberships").notNullable().defaultTo(false); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasManageGroupMembershipsCol = await knex.schema.hasColumn(TableName.OidcConfig, "manageGroupMemberships"); + + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + if (hasManageGroupMembershipsCol) { + t.dropColumn("manageGroupMemberships"); + } + }); +} diff --git a/backend/src/db/schemas/oidc-configs.ts b/backend/src/db/schemas/oidc-configs.ts index e8030267d..d7bf2f00f 100644 --- a/backend/src/db/schemas/oidc-configs.ts +++ b/backend/src/db/schemas/oidc-configs.ts @@ -27,7 +27,8 @@ export const OidcConfigsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), orgId: z.string().uuid(), - lastUsed: z.date().nullable().optional() + lastUsed: z.date().nullable().optional(), + manageGroupMemberships: z.boolean().default(false) }); export type TOidcConfigs = z.infer; diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts index cd25c5be5..71daa3446 100644 --- a/backend/src/ee/routes/v1/oidc-router.ts +++ b/backend/src/ee/routes/v1/oidc-router.ts @@ -153,7 +153,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { discoveryURL: true, isActive: true, orgId: true, - allowedEmailDomains: true + allowedEmailDomains: true, + manageGroupMemberships: true }).extend({ clientId: z.string(), clientSecret: z.string() @@ -207,7 +208,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { userinfoEndpoint: z.string().trim(), clientId: z.string().trim(), clientSecret: z.string().trim(), - isActive: z.boolean() + isActive: z.boolean(), + manageGroupMemberships: z.boolean().optional() }) .partial() .merge(z.object({ orgSlug: z.string() })), @@ -223,7 +225,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { userinfoEndpoint: true, orgId: true, allowedEmailDomains: true, - isActive: true + isActive: true, + manageGroupMemberships: true }) } }, @@ -272,7 +275,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { clientId: z.string().trim(), clientSecret: z.string().trim(), isActive: z.boolean(), - orgSlug: z.string().trim() + orgSlug: z.string().trim(), + manageGroupMemberships: z.boolean().optional().default(false) }) .superRefine((data, ctx) => { if (data.configurationType === OIDCConfigurationType.CUSTOM) { @@ -334,7 +338,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { userinfoEndpoint: true, orgId: true, isActive: true, - allowedEmailDomains: true + allowedEmailDomains: true, + manageGroupMemberships: true }) } }, @@ -350,4 +355,25 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { return oidc; } }); + + server.route({ + method: "GET", + url: "/manage-group-memberships", + schema: { + querystring: z.object({ + orgId: z.string().trim().min(1, "Org ID is required") + }), + response: { + 200: z.object({ + isEnabled: z.boolean() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const isEnabled = await server.services.oidc.isOidcManageGroupMembershipsEnabled(req.query.orgId, req.permission); + + return { isEnabled }; + } + }); }; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 6e8314731..ffabb3cc4 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -249,7 +249,9 @@ export enum EventType { DELETE_SECRET_SYNC = "delete-secret-sync", SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets", SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets", - SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets" + SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets", + OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user", + OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user" } interface UserActorMetadata { @@ -2044,6 +2046,26 @@ interface SecretSyncRemoveSecretsEvent { }; } +interface OidcGroupMembershipMappingAssignUserEvent { + type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER; + metadata: { + assignedToGroups: { id: string; name: string }[]; + userId: string; + userEmail: string; + userGroupsClaim: string[]; + }; +} + +interface OidcGroupMembershipMappingRemoveUserEvent { + type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER; + metadata: { + removedFromGroups: { id: string; name: string }[]; + userId: string; + userEmail: string; + userGroupsClaim: string[]; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -2232,4 +2254,6 @@ export type Event = | DeleteSecretSyncEvent | SecretSyncSyncSecretsEvent | SecretSyncImportSecretsEvent - | SecretSyncRemoveSecretsEvent; + | SecretSyncRemoveSecretsEvent + | OidcGroupMembershipMappingAssignUserEvent + | OidcGroupMembershipMappingRemoveUserEvent; diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index beb03f76e..7de3f8f92 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; +import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -45,6 +46,7 @@ type TGroupServiceFactoryDep = { projectKeyDAL: Pick; permissionService: Pick; licenseService: Pick; + oidcConfigDAL: Pick; }; export type TGroupServiceFactory = ReturnType; @@ -59,7 +61,8 @@ export const groupServiceFactory = ({ projectBotDAL, projectKeyDAL, permissionService, - licenseService + licenseService, + oidcConfigDAL }: TGroupServiceFactoryDep) => { const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); @@ -311,6 +314,18 @@ export const groupServiceFactory = ({ message: `Failed to find group with ID ${id}` }); + const oidcConfig = await oidcConfigDAL.findOne({ + orgId: group.orgId, + isActive: true + }); + + if (oidcConfig?.manageGroupMemberships) { + throw new BadRequestError({ + message: + "Cannot add user to group: OIDC group membership mapping is enabled - user must be assigned to this group in your OIDC provider." + }); + } + const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); // check if user has broader or equal to privileges than group @@ -366,6 +381,18 @@ export const groupServiceFactory = ({ message: `Failed to find group with ID ${id}` }); + const oidcConfig = await oidcConfigDAL.findOne({ + orgId: group.orgId, + isActive: true + }); + + if (oidcConfig?.manageGroupMemberships) { + throw new BadRequestError({ + message: + "Cannot remove user from group: OIDC group membership mapping is enabled - user must be removed from this group in your OIDC provider." + }); + } + const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); // check if user has broader or equal to privileges than group diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 45a58bd1e..0c037a2d3 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -5,6 +5,11 @@ import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; +import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; +import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; +import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -18,13 +23,18 @@ import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors"; -import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; +import { OrgServiceActor } from "@app/lib/types"; +import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; +import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; +import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { LoginMethod } from "@app/services/super-admin/super-admin-types"; @@ -45,7 +55,14 @@ import { type TOidcConfigServiceFactoryDep = { userDAL: Pick< TUserDALFactory, - "create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId" + | "create" + | "findOne" + | "updateById" + | "findById" + | "findUserEncKeyByUserId" + | "findUserEncKeyByUserIdsBatch" + | "find" + | "transaction" >; userAliasDAL: Pick; orgDAL: Pick< @@ -57,8 +74,23 @@ type TOidcConfigServiceFactoryDep = { licenseService: Pick; tokenService: Pick; smtpService: Pick; - permissionService: Pick; + permissionService: Pick; oidcConfigDAL: Pick; + groupDAL: Pick; + userGroupMembershipDAL: Pick< + TUserGroupMembershipDALFactory, + | "find" + | "transaction" + | "insertMany" + | "findGroupMembershipsByUserIdInOrg" + | "delete" + | "filterProjectsByUserMembership" + >; + groupProjectDAL: Pick; + projectKeyDAL: Pick; + projectDAL: Pick; + projectBotDAL: Pick; + auditLogService: Pick; }; export type TOidcConfigServiceFactory = ReturnType; @@ -73,7 +105,14 @@ export const oidcConfigServiceFactory = ({ tokenService, orgBotDAL, smtpService, - oidcConfigDAL + oidcConfigDAL, + userGroupMembershipDAL, + groupDAL, + groupProjectDAL, + projectKeyDAL, + projectDAL, + projectBotDAL, + auditLogService }: TOidcConfigServiceFactoryDep) => { const getOidc = async (dto: TGetOidcCfgDTO) => { const org = await orgDAL.findOne({ slug: dto.orgSlug }); @@ -156,11 +195,21 @@ export const oidcConfigServiceFactory = ({ isActive: oidcCfg.isActive, allowedEmailDomains: oidcCfg.allowedEmailDomains, clientId, - clientSecret + clientSecret, + manageGroupMemberships: oidcCfg.manageGroupMemberships }; }; - const oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => { + const oidcLogin = async ({ + externalId, + email, + firstName, + lastName, + orgId, + callbackPort, + groups = [], + manageGroupMemberships + }: TOidcLoginDTO) => { const serverCfg = await getServerCfg(); if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) { @@ -315,6 +364,83 @@ export const oidcConfigServiceFactory = ({ }); } + if (manageGroupMemberships) { + const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(user.id, orgId); + const orgGroups = await groupDAL.findByOrgId(orgId); + + const userGroupsNames = userGroups.map((membership) => membership.groupName); + const missingGroupsMemberships = groups.filter((groupName) => !userGroupsNames.includes(groupName)); + const groupsToAddUserTo = orgGroups.filter((group) => missingGroupsMemberships.includes(group.name)); + + for await (const group of groupsToAddUserTo) { + await addUsersToGroupByUserIds({ + userIds: [user.id], + group, + userDAL, + userGroupMembershipDAL, + orgDAL, + groupProjectDAL, + projectKeyDAL, + projectDAL, + projectBotDAL + }); + } + + if (groupsToAddUserTo.length) { + await auditLogService.createAuditLog({ + actor: { + type: ActorType.PLATFORM, + metadata: {} + }, + orgId, + event: { + type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER, + metadata: { + userId: user.id, + userEmail: user.email ?? user.username, + assignedToGroups: groupsToAddUserTo.map(({ id, name }) => ({ id, name })), + userGroupsClaim: groups + } + } + }); + } + + const membershipsToRemove = userGroups + .filter((membership) => !groups.includes(membership.groupName)) + .map((membership) => membership.groupId); + const groupsToRemoveUserFrom = orgGroups.filter((group) => membershipsToRemove.includes(group.id)); + + for await (const group of groupsToRemoveUserFrom) { + await removeUsersFromGroupByUserIds({ + userIds: [user.id], + group, + userDAL, + userGroupMembershipDAL, + groupProjectDAL, + projectKeyDAL + }); + } + + if (groupsToRemoveUserFrom.length) { + await auditLogService.createAuditLog({ + actor: { + type: ActorType.PLATFORM, + metadata: {} + }, + orgId, + event: { + type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER, + metadata: { + userId: user.id, + userEmail: user.email ?? user.username, + removedFromGroups: groupsToRemoveUserFrom.map(({ id, name }) => ({ id, name })), + userGroupsClaim: groups + } + } + }); + } + } + await licenseService.updateSubscriptionOrgMemberCount(organization.id); const userEnc = await userDAL.findUserEncKeyByUserId(user.id); @@ -385,7 +511,8 @@ export const oidcConfigServiceFactory = ({ tokenEndpoint, userinfoEndpoint, clientId, - clientSecret + clientSecret, + manageGroupMemberships }: TUpdateOidcCfgDTO) => { const org = await orgDAL.findOne({ slug: orgSlug @@ -448,7 +575,8 @@ export const oidcConfigServiceFactory = ({ userinfoEndpoint, jwksUri, isActive, - lastUsed: null + lastUsed: null, + manageGroupMemberships }; if (clientId !== undefined) { @@ -491,7 +619,8 @@ export const oidcConfigServiceFactory = ({ tokenEndpoint, userinfoEndpoint, clientId, - clientSecret + clientSecret, + manageGroupMemberships }: TCreateOidcCfgDTO) => { const org = await orgDAL.findOne({ slug: orgSlug @@ -589,7 +718,8 @@ export const oidcConfigServiceFactory = ({ clientIdTag, encryptedClientSecret, clientSecretIV, - clientSecretTag + clientSecretTag, + manageGroupMemberships }); return oidcCfg; @@ -683,7 +813,9 @@ export const oidcConfigServiceFactory = ({ firstName: claims.given_name ?? "", lastName: claims.family_name ?? "", orgId: org.id, - callbackPort + groups: claims.groups as string[] | undefined, + callbackPort, + manageGroupMemberships: oidcCfg.manageGroupMemberships }) .then(({ isUserCompleted, providerAuthToken }) => { cb(null, { isUserCompleted, providerAuthToken }); @@ -697,5 +829,16 @@ export const oidcConfigServiceFactory = ({ return strategy; }; - return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg }; + const isOidcManageGroupMembershipsEnabled = async (orgId: string, actor: OrgServiceActor) => { + await permissionService.getUserOrgPermission(actor.id, orgId, actor.authMethod, actor.orgId); + + const oidcConfig = await oidcConfigDAL.findOne({ + orgId, + isActive: true + }); + + return Boolean(oidcConfig?.manageGroupMemberships); + }; + + return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg, isOidcManageGroupMembershipsEnabled }; }; diff --git a/backend/src/ee/services/oidc/oidc-config-types.ts b/backend/src/ee/services/oidc/oidc-config-types.ts index 6e36b796b..a6bd6ad67 100644 --- a/backend/src/ee/services/oidc/oidc-config-types.ts +++ b/backend/src/ee/services/oidc/oidc-config-types.ts @@ -12,6 +12,8 @@ export type TOidcLoginDTO = { lastName?: string; orgId: string; callbackPort?: string; + groups?: string[]; + manageGroupMemberships?: boolean | null; }; export type TGetOidcCfgDTO = @@ -37,6 +39,7 @@ export type TCreateOidcCfgDTO = { clientSecret: string; isActive: boolean; orgSlug: string; + manageGroupMemberships: boolean; } & TGenericPermission; export type TUpdateOidcCfgDTO = Partial<{ @@ -52,5 +55,6 @@ export type TUpdateOidcCfgDTO = Partial<{ clientSecret: string; isActive: boolean; orgSlug: string; + manageGroupMemberships: boolean; }> & TGenericPermission; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 442f11080..3e5947956 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -467,7 +467,8 @@ export const registerRoutes = async ( projectBotDAL, projectKeyDAL, permissionService, - licenseService + licenseService, + oidcConfigDAL }); const groupProjectService = groupProjectServiceFactory({ groupDAL, @@ -1337,7 +1338,14 @@ export const registerRoutes = async ( smtpService, orgBotDAL, permissionService, - oidcConfigDAL + oidcConfigDAL, + projectBotDAL, + projectKeyDAL, + projectDAL, + userGroupMembershipDAL, + groupProjectDAL, + groupDAL, + auditLogService }); const userEngagementService = userEngagementServiceFactory({ diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index a9ff7e091..40d17c1b0 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -192,6 +192,17 @@ services: depends_on: - openldap profiles: [ldap] + + keycloak: + image: quay.io/keycloak/keycloak:26.1.0 + restart: always + environment: + - KC_BOOTSTRAP_ADMIN_PASSWORD=admin + - KC_BOOTSTRAP_ADMIN_USERNAME=admin + command: start-dev + ports: + - 8088:8080 + profiles: [ sso ] volumes: postgres-data: diff --git a/docs/documentation/platform/sso/keycloak-oidc.mdx b/docs/documentation/platform/sso/keycloak-oidc.mdx deleted file mode 100644 index cb774a014..000000000 --- a/docs/documentation/platform/sso/keycloak-oidc.mdx +++ /dev/null @@ -1,112 +0,0 @@ ---- -title: "Keycloak OIDC" -description: "Learn how to configure Keycloak OIDC for Infisical SSO." ---- - - - Keycloak OIDC SSO is a paid feature. If you're using Infisical Cloud, then it - is available under the **Pro Tier**. If you're self-hosting Infisical, then - you should contact sales@infisical.com to purchase an enterprise license to - use it. - - - - - 1.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application. - - ![OIDC keycloak list of clients](../../../images/sso/keycloak-oidc/clients-list.png) - - - You don’t typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm. - - - 1.2. In the General Settings step, set **Client type** to **OpenID Connect**, the **Client ID** field to an appropriate identifier, and the **Name** field to a friendly name like **Infisical**. - - ![OIDC keycloak create client general settings](../../../images/sso/keycloak-oidc/create-client-general-settings.png) - - 1.3. Next, in the Capability Config step, ensure that **Client Authentication** is set to On and that **Standard flow** is enabled in the Authentication flow section. - - ![OIDC keycloak create client capability config settings](../../../images/sso/keycloak-oidc/create-client-capability.png) - - 1.4. In the Login Settings step, set the following values: - - Root URL: `https://app.infisical.com`. - - Home URL: `https://app.infisical.com`. - - Valid Redirect URIs: `https://app.infisical.com/api/v1/sso/oidc/callback`. - - Web origins: `https://app.infisical.com`. - - ![OIDC keycloak create client login settings](../../../images/sso/keycloak-oidc/create-client-login-settings.png) - - If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com (base URL) with your own domain. - - - 1.5. Next, navigate to the **Client scopes** tab and select the client's dedicated scope. - - ![OIDC keycloak client scopes list](../../../images/sso/keycloak-oidc/client-scope-list.png) - - 1.6. Next, click **Add predefined mapper**. - - ![OIDC keycloak client mappers empty](../../../images/sso/keycloak-oidc/client-scope-mapper-menu.png) - - 1.7. Select the **email**, **given name**, **family name** attributes and click **Add**. - - ![OIDC keycloak client mappers predefined 1](../../../images/sso/keycloak-oidc/scope-predefined-mapper-1.png) - ![OIDC keycloak client mappers predefined 2](../../../images/sso/keycloak-oidc/scope-predefined-mapper-2.png) - - Once you've completed the above steps, the list of mappers should look like the following: - ![OIDC keycloak client mappers completed](../../../images/sso/keycloak-oidc/client-scope-complete-overview.png) - - - - 2.1. Back in Keycloak, navigate to Configure > Realm settings > General tab > Endpoints > OpenID Endpoint Configuration and copy the opened URL. This is what is to referred to as the Discovery Document URL and it takes the form: `https://keycloak-mysite.com/realms/myrealm/.well-known/openid-configuration`. - ![OIDC keycloak realm OIDC metadata](../../../images/sso/keycloak-oidc/realm-setting-oidc-config.png) - - 2.2. From the Clients page, navigate to the Credential tab and copy the **Client Secret** to be used in the next steps. - ![OIDC keycloak realm OIDC secret](../../../images/sso/keycloak-oidc/client-secret.png) - - - - 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect. - ![OIDC keycloak manage org Infisical](../../../images/sso/keycloak-oidc/manage-org-oidc.png) - - 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**. - ![OIDC keycloak paste values into Infisical](../../../images/sso/keycloak-oidc/create-oidc.png) - - Once you've done that, press **Update** to complete the required configuration. - - - - Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak. - - ![OIDC keycloak enable OIDC](../../../images/sso/keycloak-oidc/enable-oidc.png) - - - - Enforcing OIDC SSO ensures that members in your organization can only access Infisical - by logging into the organization via Keycloak. - - To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Keycloak user with Infisical. - Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO. - - - We recommend ensuring that your account is provisioned using the application in Keycloak - prior to enforcing OIDC SSO to prevent any unintended issues. - - - - - - - If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login. - - - - If you're configuring OIDC SSO on a self-hosted instance of Infisical, make - sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to - work: -
- - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This - can be a random 32-byte base64 string generated with `openssl rand -base64 - 32`. -
- - `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com) - diff --git a/docs/documentation/platform/sso/keycloak-oidc/group-membership-mapping.mdx b/docs/documentation/platform/sso/keycloak-oidc/group-membership-mapping.mdx new file mode 100644 index 000000000..c423bac5a --- /dev/null +++ b/docs/documentation/platform/sso/keycloak-oidc/group-membership-mapping.mdx @@ -0,0 +1,62 @@ +--- +title: "Keycloak OIDC Group Membership Mapping" +sidebarTitle: "Group Membership Mapping" +description: "Learn how to sync Keycloak group members to matching groups in Infisical." +--- + +You can have Infisical automatically sync group +memberships between Keycloak and Infisical by configuring a group membership mapper in Keycloak. +When a user logs in via OIDC, they will be added to Infisical groups that match their Keycloak groups names, and removed from any +Infisical groups not present in their groups claim. + + + When enabled, manual + management of Infisical group memberships will be disabled. + + + + Group membership changes in the Keycloak only sync with Infisical when a + user logs in via OIDC. For example, if you remove a user from a group in Keycloak, this change will not be reflected in Infisical until their next OIDC login. To ensure this behavior, Infisical recommends enabling Enforce OIDC + SSO in the OIDC settings. + + + + + + 1.1. In your realm, navigate to the **Clients** tab and select your Infisical client. + + ![OIDC keycloak client](/images/sso/keycloak-oidc/group-membership-mapping/select-client.png) + + 1.2. Select the **Client Scopes** tab. + + ![OIDC keycloak client scopes](/images/sso/keycloak-oidc/group-membership-mapping/select-client-scopes.png) + + 1.3. Next, select the dedicated scope for your Infisical client. + + ![OIDC keycloak dedicated scope](/images/sso/keycloak-oidc/group-membership-mapping/select-dedicated-scope.png) + + 1.4. Click on the **Add mapper** button, and select the **By configuration** option. + + ![OIDC keycloak add mapper by configuration](/images/sso/keycloak-oidc/group-membership-mapping/create-mapper-by-configuration.png) + + 1.5. Select the **Group Membership** option. + + ![OIDC keycloak group membership option](/images/sso/keycloak-oidc/group-membership-mapping/select-group-membership-mapper.png) + + 1.6. Give your mapper a name and ensure the following properties are set to the following before saving: + - **Token Claim Name** is set to `groups` + - **Full group path** is disabled + + ![OIDC keycloak group membership mapper](/images/sso/keycloak-oidc/group-membership-mapping/create-group-membership-mapper.png) + + + 2.1. In Infisical, create any groups you would like to sync users to. Make sure the name of the Infisical group is an exact match of the Keycloak group name. + ![OIDC keycloak infisical group](/images/sso/keycloak-oidc/group-membership-mapping/create-infisical-group.png) + + 2.2. Next, enable **OIDC Group Membership Mapping** in Organization Settings > Security. + ![OIDC keycloak enable group membership mapping](/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png) + + 2.3. The next time a user logs in they will be synced to their matching Keycloak groups. + ![OIDC keycloak synced users](/images/sso/keycloak-oidc/group-membership-mapping/synced-users.png) + + \ No newline at end of file diff --git a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx new file mode 100644 index 000000000..3b7df8990 --- /dev/null +++ b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx @@ -0,0 +1,113 @@ +--- +title: "Keycloak OIDC Overview" +sidebarTitle: "Overview" +description: "Learn how to configure Keycloak OIDC for Infisical SSO." +--- + + + Keycloak OIDC SSO is a paid feature. If you're using Infisical Cloud, then it + is available under the **Pro Tier**. If you're self-hosting Infisical, then + you should contact sales@infisical.com to purchase an enterprise license to + use it. + + + + + 1.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application. + + ![OIDC keycloak list of clients](../../../images/sso/keycloak-oidc/clients-list.png) + + + You don’t typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm. + + + 1.2. In the General Settings step, set **Client type** to **OpenID Connect**, the **Client ID** field to an appropriate identifier, and the **Name** field to a friendly name like **Infisical**. + + ![OIDC keycloak create client general settings](../../../images/sso/keycloak-oidc/create-client-general-settings.png) + + 1.3. Next, in the Capability Config step, ensure that **Client Authentication** is set to On and that **Standard flow** is enabled in the Authentication flow section. + + ![OIDC keycloak create client capability config settings](../../../images/sso/keycloak-oidc/create-client-capability.png) + + 1.4. In the Login Settings step, set the following values: + - Root URL: `https://app.infisical.com`. + - Home URL: `https://app.infisical.com`. + - Valid Redirect URIs: `https://app.infisical.com/api/v1/sso/oidc/callback`. + - Web origins: `https://app.infisical.com`. + + ![OIDC keycloak create client login settings](../../../images/sso/keycloak-oidc/create-client-login-settings.png) + + If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com (base URL) with your own domain. + + + 1.5. Next, navigate to the **Client scopes** tab and select the client's dedicated scope. + + ![OIDC keycloak client scopes list](../../../images/sso/keycloak-oidc/client-scope-list.png) + + 1.6. Next, click **Add predefined mapper**. + + ![OIDC keycloak client mappers empty](../../../images/sso/keycloak-oidc/client-scope-mapper-menu.png) + + 1.7. Select the **email**, **given name**, **family name** attributes and click **Add**. + + ![OIDC keycloak client mappers predefined 1](../../../images/sso/keycloak-oidc/scope-predefined-mapper-1.png) + ![OIDC keycloak client mappers predefined 2](../../../images/sso/keycloak-oidc/scope-predefined-mapper-2.png) + + Once you've completed the above steps, the list of mappers should look like the following: + ![OIDC keycloak client mappers completed](../../../images/sso/keycloak-oidc/client-scope-complete-overview.png) + + + + 2.1. Back in Keycloak, navigate to Configure > Realm settings > General tab > Endpoints > OpenID Endpoint Configuration and copy the opened URL. This is what is to referred to as the Discovery Document URL and it takes the form: `https://keycloak-mysite.com/realms/myrealm/.well-known/openid-configuration`. + ![OIDC keycloak realm OIDC metadata](../../../images/sso/keycloak-oidc/realm-setting-oidc-config.png) + + 2.2. From the Clients page, navigate to the Credential tab and copy the **Client Secret** to be used in the next steps. + ![OIDC keycloak realm OIDC secret](../../../images/sso/keycloak-oidc/client-secret.png) + + + + 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect. + ![OIDC keycloak manage org Infisical](../../../images/sso/keycloak-oidc/manage-org-oidc.png) + + 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**. + ![OIDC keycloak paste values into Infisical](../../../images/sso/keycloak-oidc/create-oidc.png) + + Once you've done that, press **Update** to complete the required configuration. + + + + Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak. + + ![OIDC keycloak enable OIDC](../../../images/sso/keycloak-oidc/enable-oidc.png) + + + + Enforcing OIDC SSO ensures that members in your organization can only access Infisical + by logging into the organization via Keycloak. + + To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Keycloak user with Infisical. + Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO. + + + We recommend ensuring that your account is provisioned using the application in Keycloak + prior to enforcing OIDC SSO to prevent any unintended issues. + + + + + + + If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login. + + + + If you're configuring OIDC SSO on a self-hosted instance of Infisical, make + sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to + work: +
+ - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This + can be a random 32-byte base64 string generated with `openssl rand -base64 + 32`. +
+ - `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com) + diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/create-group-membership-mapper.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/create-group-membership-mapper.png new file mode 100644 index 000000000..33e283891 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/create-group-membership-mapper.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/create-infisical-group.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/create-infisical-group.png new file mode 100644 index 000000000..6315631e5 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/create-infisical-group.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/create-mapper-by-configuration.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/create-mapper-by-configuration.png new file mode 100644 index 000000000..f2d358be1 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/create-mapper-by-configuration.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png new file mode 100644 index 000000000..199a7432a Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/select-client-scopes.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-client-scopes.png new file mode 100644 index 000000000..ddd8b84be Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-client-scopes.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/select-client.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-client.png new file mode 100644 index 000000000..eb910a876 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-client.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/select-dedicated-scope.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-dedicated-scope.png new file mode 100644 index 000000000..1580a9d61 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-dedicated-scope.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/select-group-membership-mapper.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-group-membership-mapper.png new file mode 100644 index 000000000..cafae7666 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-group-membership-mapper.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/synced-users.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/synced-users.png new file mode 100644 index 000000000..37193e2fe Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/synced-users.png differ diff --git a/docs/internals/permissions.mdx b/docs/internals/permissions.mdx index 1fce09def..737d3b71e 100644 --- a/docs/internals/permissions.mdx +++ b/docs/internals/permissions.mdx @@ -52,6 +52,7 @@ Refer to the table below for a list of subjects and the actions they support. | `pki-collections` | `read`, `create`, `edit`, `delete` | | `kms` | `edit` | | `cmek` | `read`, `create`, `edit`, `delete`, `encrypt`, `decrypt` | +| `secret-syncs` | `read`, `create`, `edit`, `delete`, `sync-secrets`, `import-secrets`, `remove-secrets` | @@ -63,21 +64,23 @@ Refer to the table below for a list of subjects and the actions they support. `read`, `create`, `edit`, `delete`. -| Subject | Actions | -| ------------------ | ---------------------------------- | -| `workspace` | `read`, `create` | -| `role` | `read`, `create`, `edit`, `delete` | -| `member` | `read`, `create`, `edit`, `delete` | -| `secret-scanning` | `read`, `create`, `edit`, `delete` | -| `settings` | `read`, `create`, `edit`, `delete` | -| `incident-account` | `read`, `create`, `edit`, `delete` | -| `sso` | `read`, `create`, `edit`, `delete` | -| `scim` | `read`, `create`, `edit`, `delete` | -| `ldap` | `read`, `create`, `edit`, `delete` | -| `groups` | `read`, `create`, `edit`, `delete` | -| `billing` | `read`, `create`, `edit`, `delete` | -| `identity` | `read`, `create`, `edit`, `delete` | -| `kms` | `read` | +| Subject | Actions | +| --------------------- | ------------------------------------------------ | +| `workspace` | `read`, `create` | +| `role` | `read`, `create`, `edit`, `delete` | +| `member` | `read`, `create`, `edit`, `delete` | +| `secret-scanning` | `read`, `create`, `edit`, `delete` | +| `settings` | `read`, `create`, `edit`, `delete` | +| `incident-account` | `read`, `create`, `edit`, `delete` | +| `sso` | `read`, `create`, `edit`, `delete` | +| `scim` | `read`, `create`, `edit`, `delete` | +| `ldap` | `read`, `create`, `edit`, `delete` | +| `groups` | `read`, `create`, `edit`, `delete` | +| `billing` | `read`, `create`, `edit`, `delete` | +| `identity` | `read`, `create`, `edit`, `delete` | +| `project-templates` | `read`, `create`, `edit`, `delete` | +| `app-connections` | `read`, `create`, `edit`, `delete`, `connect` | +| `kms` | `read` | @@ -90,7 +93,6 @@ Permission inversion allows you to explicitly deny actions instead of allowing t - secret-folders - secret-imports - dynamic-secrets -- cmek When a permission is inverted, it changes from an "allow" rule to a "deny" rule. For example: diff --git a/docs/mint.json b/docs/mint.json index 31c5d7b14..215ca24da 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -249,7 +249,13 @@ "documentation/platform/sso/keycloak-saml", "documentation/platform/sso/google-saml", "documentation/platform/sso/auth0-saml", - "documentation/platform/sso/keycloak-oidc", + { + "group": "Keycloak OIDC", + "pages": [ + "documentation/platform/sso/keycloak-oidc/overview", + "documentation/platform/sso/keycloak-oidc/group-membership-mapping" + ] + }, "documentation/platform/sso/auth0-oidc", "documentation/platform/sso/general-oidc" ] diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index c736a564e..6a990f80e 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -114,7 +114,11 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.DELETE_SECRET_SYNC]: "Delete Secret Sync", [EventType.SECRET_SYNC_SYNC_SECRETS]: "Secret Sync synced secrets", [EventType.SECRET_SYNC_IMPORT_SECRETS]: "Secret Sync imported secrets", - [EventType.SECRET_SYNC_REMOVE_SECRETS]: "Secret Sync removed secrets" + [EventType.SECRET_SYNC_REMOVE_SECRETS]: "Secret Sync removed secrets", + [EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER]: + "OIDC group membership mapping assigned user to groups", + [EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]: + "OIDC group membership mapping removed user from groups" }; export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = { diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 7219a446e..349811180 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -127,5 +127,7 @@ export enum EventType { DELETE_SECRET_SYNC = "delete-secret-sync", SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets", SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets", - SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets" + SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets", + OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user", + OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user" } diff --git a/frontend/src/hooks/api/oidcConfig/mutations.tsx b/frontend/src/hooks/api/oidcConfig/mutations.tsx index b29ebd333..9c7927458 100644 --- a/frontend/src/hooks/api/oidcConfig/mutations.tsx +++ b/frontend/src/hooks/api/oidcConfig/mutations.tsx @@ -20,7 +20,8 @@ export const useUpdateOIDCConfig = () => { clientId, clientSecret, isActive, - orgSlug + orgSlug, + manageGroupMemberships }: { allowedEmailDomains?: string; issuer?: string; @@ -34,6 +35,7 @@ export const useUpdateOIDCConfig = () => { isActive?: boolean; configurationType?: string; orgSlug: string; + manageGroupMemberships?: boolean; }) => { const { data } = await apiRequest.patch("/api/v1/sso/oidc/config", { issuer, @@ -47,7 +49,8 @@ export const useUpdateOIDCConfig = () => { clientId, orgSlug, clientSecret, - isActive + isActive, + manageGroupMemberships }); return data; @@ -74,7 +77,8 @@ export const useCreateOIDCConfig = () => { clientId, clientSecret, isActive, - orgSlug + orgSlug, + manageGroupMemberships }: { issuer?: string; configurationType: string; @@ -88,6 +92,7 @@ export const useCreateOIDCConfig = () => { isActive: boolean; orgSlug: string; allowedEmailDomains?: string; + manageGroupMemberships?: boolean; }) => { const { data } = await apiRequest.post("/api/v1/sso/oidc/config", { issuer, @@ -101,7 +106,8 @@ export const useCreateOIDCConfig = () => { clientId, clientSecret, isActive, - orgSlug + orgSlug, + manageGroupMemberships }); return data; diff --git a/frontend/src/hooks/api/oidcConfig/queries.tsx b/frontend/src/hooks/api/oidcConfig/queries.tsx index e22d2ee2f..38c939520 100644 --- a/frontend/src/hooks/api/oidcConfig/queries.tsx +++ b/frontend/src/hooks/api/oidcConfig/queries.tsx @@ -5,7 +5,9 @@ import { apiRequest } from "@app/config/request"; import { OIDCConfigData } from "./types"; export const oidcConfigKeys = { - getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const + getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const, + getOIDCManageGroupMembershipsEnabled: (orgId: string) => + ["oidc-manage-group-memberships", orgId] as const }; export const useGetOIDCConfig = (orgSlug: string) => { @@ -25,3 +27,16 @@ export const useGetOIDCConfig = (orgSlug: string) => { enabled: true }); }; + +export const useOidcManageGroupMembershipsEnabled = (orgId: string) => { + return useQuery({ + queryKey: oidcConfigKeys.getOIDCManageGroupMembershipsEnabled(orgId), + queryFn: async () => { + const { data } = await apiRequest.get<{ isEnabled: boolean }>( + `/api/v1/sso/oidc/manage-group-memberships?orgId=${orgId}` + ); + + return data.isEnabled; + } + }); +}; diff --git a/frontend/src/hooks/api/oidcConfig/types.ts b/frontend/src/hooks/api/oidcConfig/types.ts index 1b8ede5e3..3359e4487 100644 --- a/frontend/src/hooks/api/oidcConfig/types.ts +++ b/frontend/src/hooks/api/oidcConfig/types.ts @@ -12,4 +12,5 @@ export type OIDCConfigData = { clientId: string; clientSecret: string; allowedEmailDomains?: string; + manageGroupMemberships: boolean; }; diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx index 5e663f283..f7c7398ee 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsTableRow.tsx @@ -40,6 +40,12 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop

Machine Identity

); + case ActorType.PLATFORM: + return ( + +

Platform

+ + ); case ActorType.UNKNOWN_USER: return ( diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx index 33a5e1f54..04b8d0197 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx @@ -2,8 +2,10 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; -import { DeleteActionModal, IconButton } from "@app/components/v2"; -import { useRemoveUserFromGroup } from "@app/hooks/api"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { DeleteActionModal, IconButton, Tooltip } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { useOidcManageGroupMembershipsEnabled, useRemoveUserFromGroup } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; import { AddGroupMembersModal } from "../AddGroupMemberModal"; @@ -20,6 +22,11 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => { "removeMemberFromGroup" ] as const); + const { currentOrg } = useOrganization(); + + const { data: isOidcManageGroupMembershipsEnabled = false } = + useOidcManageGroupMembershipsEnabled(currentOrg.id); + const { mutateAsync: removeUserFromGroupMutateAsync } = useRemoveUserFromGroup(); const handleRemoveUserFromGroup = async (username: string) => { try { @@ -47,19 +54,35 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {

Group Members

- { - handlePopUpOpen("addGroupMembers", { - groupId, - slug: groupSlug - }); - }} - > - - + + {(isAllowed) => ( + +
+ { + handlePopUpOpen("addGroupMembers", { + groupId, + slug: groupSlug + }); + }} + > + + +
+
+ )} +
{(isAllowed) => ( -
- -
+ +
+ +
+
)} )} diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx index 2aa9f4ef5..0f3c67f8c 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx @@ -3,7 +3,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { OrgPermissionCan } from "@app/components/permissions"; import { IconButton, Td, Tooltip, Tr } from "@app/components/v2"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { useOidcManageGroupMembershipsEnabled } from "@app/hooks/api"; import { TGroupUser } from "@app/hooks/api/groups/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -19,6 +20,11 @@ export const GroupMembershipRow = ({ user: { firstName, lastName, username, joinedGroupAt, email, id }, handlePopUpOpen }: Props) => { + const { currentOrg } = useOrganization(); + + const { data: isOidcManageGroupMembershipsEnabled = false } = + useOidcManageGroupMembershipsEnabled(currentOrg.id); + return ( @@ -36,15 +42,21 @@ export const GroupMembershipRow = ({ {(isAllowed) => { return ( - + handlePopUpOpen("removeMemberFromGroup", { username })} variant="plain" colorSchema="danger" > - + ); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgOIDCSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgOIDCSection.tsx index 2c0232d7c..bec65bd8f 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgOIDCSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgOIDCSection.tsx @@ -1,7 +1,10 @@ +import { faInfoCircle, faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; -import { Button, Switch } from "@app/components/v2"; +import { Button, Switch, Tooltip } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects, @@ -79,6 +82,29 @@ export const OrgOIDCSection = (): JSX.Element => { } }; + const handleOIDCGroupManagement = async (value: boolean) => { + try { + if (!currentOrg?.id) return; + + if (!subscription?.oidcSSO) { + handlePopUpOpen("upgradePlan"); + return; + } + + await mutateAsync({ + orgSlug: currentOrg?.slug, + manageGroupMemberships: value + }); + + createNotification({ + text: `Successfully ${value ? "enabled" : "disabled"} OIDC group membership mapping`, + type: "success" + }); + } catch (err) { + console.error(err); + } + }; + const addOidcButtonClick = async () => { if (subscription?.oidcSSO && currentOrg) { handlePopUpOpen("addOIDC"); @@ -148,6 +174,65 @@ export const OrgOIDCSection = (): JSX.Element => { Enforce members to authenticate via OIDC to access this organization

+
+
+
+ OIDC Group Membership Mapping + +

+ When this feature is enabled, Infisical will automatically sync group + memberships between the OIDC provider and Infisical. Users will be added to + Infisical groups that match their OIDC group names, and removed from any + Infisical groups not present in their groups claim. When enabled, manual + management of Infisical group memberships will be disabled. +

+

+ To use this feature you must include group claims in the OIDC token. +

+ + See your OIDC provider docs for details. + +

+ + Group membership changes in the OIDC provider only sync with Infisical when a + user logs in via OIDC. For example, if you remove a user from a group in the + OIDC provider, this change will not be reflected in Infisical until their next + OIDC login. To ensure this behavior, Infisical recommends enabling Enforce OIDC + SSO. +

+ + } + > + +
+
+ + {(isAllowed) => ( + handleOIDCGroupManagement(value)} + isDisabled={!isAllowed} + /> + )} + +
+

+ Infisical will manage user group memberships based on the OIDC provider +

+