feature: oidc group membership mapping
@@ -0,0 +1,23 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
const hasManageGroupMembershipsCol = await knex.schema.hasColumn(TableName.OidcConfig, "manageGroupMemberships");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
|
||||||
|
if (hasManageGroupMembershipsCol) {
|
||||||
|
t.dropColumn("manageGroupMemberships");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -27,7 +27,8 @@ export const OidcConfigsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
lastUsed: z.date().nullable().optional()
|
lastUsed: z.date().nullable().optional(),
|
||||||
|
manageGroupMemberships: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
||||||
|
|||||||
@@ -153,7 +153,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
discoveryURL: true,
|
discoveryURL: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
orgId: true,
|
orgId: true,
|
||||||
allowedEmailDomains: true
|
allowedEmailDomains: true,
|
||||||
|
manageGroupMemberships: true
|
||||||
}).extend({
|
}).extend({
|
||||||
clientId: z.string(),
|
clientId: z.string(),
|
||||||
clientSecret: z.string()
|
clientSecret: z.string()
|
||||||
@@ -207,7 +208,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
userinfoEndpoint: z.string().trim(),
|
userinfoEndpoint: z.string().trim(),
|
||||||
clientId: z.string().trim(),
|
clientId: z.string().trim(),
|
||||||
clientSecret: z.string().trim(),
|
clientSecret: z.string().trim(),
|
||||||
isActive: z.boolean()
|
isActive: z.boolean(),
|
||||||
|
manageGroupMemberships: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
.merge(z.object({ orgSlug: z.string() })),
|
.merge(z.object({ orgSlug: z.string() })),
|
||||||
@@ -223,7 +225,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
userinfoEndpoint: true,
|
userinfoEndpoint: true,
|
||||||
orgId: true,
|
orgId: true,
|
||||||
allowedEmailDomains: true,
|
allowedEmailDomains: true,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
manageGroupMemberships: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -272,7 +275,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
clientId: z.string().trim(),
|
clientId: z.string().trim(),
|
||||||
clientSecret: z.string().trim(),
|
clientSecret: z.string().trim(),
|
||||||
isActive: z.boolean(),
|
isActive: z.boolean(),
|
||||||
orgSlug: z.string().trim()
|
orgSlug: z.string().trim(),
|
||||||
|
manageGroupMemberships: z.boolean().optional().default(false)
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
if (data.configurationType === OIDCConfigurationType.CUSTOM) {
|
if (data.configurationType === OIDCConfigurationType.CUSTOM) {
|
||||||
@@ -334,7 +338,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
userinfoEndpoint: true,
|
userinfoEndpoint: true,
|
||||||
orgId: true,
|
orgId: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
allowedEmailDomains: true
|
allowedEmailDomains: true,
|
||||||
|
manageGroupMemberships: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -350,4 +355,25 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
return oidc;
|
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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||||
|
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
@@ -45,6 +46,7 @@ type TGroupServiceFactoryDep = {
|
|||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
|
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
|
||||||
@@ -59,7 +61,8 @@ export const groupServiceFactory = ({
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
oidcConfigDAL
|
||||||
}: TGroupServiceFactoryDep) => {
|
}: TGroupServiceFactoryDep) => {
|
||||||
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
|
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
|
||||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||||
@@ -311,6 +314,15 @@ export const groupServiceFactory = ({
|
|||||||
message: `Failed to find group with ID ${id}`
|
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." });
|
||||||
|
}
|
||||||
|
|
||||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||||
|
|
||||||
// check if user has broader or equal to privileges than group
|
// check if user has broader or equal to privileges than group
|
||||||
@@ -366,6 +378,17 @@ export const groupServiceFactory = ({
|
|||||||
message: `Failed to find group with ID ${id}`
|
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."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||||
|
|
||||||
// check if user has broader or equal to privileges than group
|
// check if user has broader or equal to privileges than group
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
auditLogStreamLimit: 3,
|
auditLogStreamLimit: 3,
|
||||||
samlSSO: false,
|
samlSSO: false,
|
||||||
hsm: false,
|
hsm: false,
|
||||||
oidcSSO: false,
|
oidcSSO: true,
|
||||||
scim: false,
|
scim: false,
|
||||||
ldap: false,
|
ldap: false,
|
||||||
groups: false,
|
groups: true,
|
||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet }
|
|||||||
|
|
||||||
import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
||||||
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
|
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
|
||||||
|
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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
@@ -18,13 +21,18 @@ import {
|
|||||||
infisicalSymmetricEncypt
|
infisicalSymmetricEncypt
|
||||||
} from "@app/lib/crypto/encryption";
|
} from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
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 { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
|
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
|
||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
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 { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
|
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
|
||||||
@@ -45,7 +53,14 @@ import {
|
|||||||
type TOidcConfigServiceFactoryDep = {
|
type TOidcConfigServiceFactoryDep = {
|
||||||
userDAL: Pick<
|
userDAL: Pick<
|
||||||
TUserDALFactory,
|
TUserDALFactory,
|
||||||
"create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId"
|
| "create"
|
||||||
|
| "findOne"
|
||||||
|
| "updateById"
|
||||||
|
| "findById"
|
||||||
|
| "findUserEncKeyByUserId"
|
||||||
|
| "findUserEncKeyByUserIdsBatch"
|
||||||
|
| "find"
|
||||||
|
| "transaction"
|
||||||
>;
|
>;
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||||
orgDAL: Pick<
|
orgDAL: Pick<
|
||||||
@@ -57,8 +72,22 @@ type TOidcConfigServiceFactoryDep = {
|
|||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail" | "verify">;
|
smtpService: Pick<TSmtpService, "sendMail" | "verify">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getUserOrgPermission">;
|
||||||
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
|
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
|
||||||
|
groupDAL: Pick<TGroupDALFactory, "findByOrgId">;
|
||||||
|
userGroupMembershipDAL: Pick<
|
||||||
|
TUserGroupMembershipDALFactory,
|
||||||
|
| "find"
|
||||||
|
| "transaction"
|
||||||
|
| "insertMany"
|
||||||
|
| "findGroupMembershipsByUserIdInOrg"
|
||||||
|
| "delete"
|
||||||
|
| "filterProjectsByUserMembership"
|
||||||
|
>;
|
||||||
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||||
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOidcConfigServiceFactory = ReturnType<typeof oidcConfigServiceFactory>;
|
export type TOidcConfigServiceFactory = ReturnType<typeof oidcConfigServiceFactory>;
|
||||||
@@ -73,7 +102,13 @@ export const oidcConfigServiceFactory = ({
|
|||||||
tokenService,
|
tokenService,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
oidcConfigDAL
|
oidcConfigDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
groupDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL
|
||||||
}: TOidcConfigServiceFactoryDep) => {
|
}: TOidcConfigServiceFactoryDep) => {
|
||||||
const getOidc = async (dto: TGetOidcCfgDTO) => {
|
const getOidc = async (dto: TGetOidcCfgDTO) => {
|
||||||
const org = await orgDAL.findOne({ slug: dto.orgSlug });
|
const org = await orgDAL.findOne({ slug: dto.orgSlug });
|
||||||
@@ -156,11 +191,21 @@ export const oidcConfigServiceFactory = ({
|
|||||||
isActive: oidcCfg.isActive,
|
isActive: oidcCfg.isActive,
|
||||||
allowedEmailDomains: oidcCfg.allowedEmailDomains,
|
allowedEmailDomains: oidcCfg.allowedEmailDomains,
|
||||||
clientId,
|
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();
|
const serverCfg = await getServerCfg();
|
||||||
|
|
||||||
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) {
|
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) {
|
||||||
@@ -315,6 +360,45 @@ 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||||
|
|
||||||
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||||
@@ -385,7 +469,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
tokenEndpoint,
|
tokenEndpoint,
|
||||||
userinfoEndpoint,
|
userinfoEndpoint,
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret
|
clientSecret,
|
||||||
|
manageGroupMemberships
|
||||||
}: TUpdateOidcCfgDTO) => {
|
}: TUpdateOidcCfgDTO) => {
|
||||||
const org = await orgDAL.findOne({
|
const org = await orgDAL.findOne({
|
||||||
slug: orgSlug
|
slug: orgSlug
|
||||||
@@ -448,7 +533,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
userinfoEndpoint,
|
userinfoEndpoint,
|
||||||
jwksUri,
|
jwksUri,
|
||||||
isActive,
|
isActive,
|
||||||
lastUsed: null
|
lastUsed: null,
|
||||||
|
manageGroupMemberships
|
||||||
};
|
};
|
||||||
|
|
||||||
if (clientId !== undefined) {
|
if (clientId !== undefined) {
|
||||||
@@ -491,7 +577,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
tokenEndpoint,
|
tokenEndpoint,
|
||||||
userinfoEndpoint,
|
userinfoEndpoint,
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret
|
clientSecret,
|
||||||
|
manageGroupMemberships
|
||||||
}: TCreateOidcCfgDTO) => {
|
}: TCreateOidcCfgDTO) => {
|
||||||
const org = await orgDAL.findOne({
|
const org = await orgDAL.findOne({
|
||||||
slug: orgSlug
|
slug: orgSlug
|
||||||
@@ -589,7 +676,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
clientIdTag,
|
clientIdTag,
|
||||||
encryptedClientSecret,
|
encryptedClientSecret,
|
||||||
clientSecretIV,
|
clientSecretIV,
|
||||||
clientSecretTag
|
clientSecretTag,
|
||||||
|
manageGroupMemberships
|
||||||
});
|
});
|
||||||
|
|
||||||
return oidcCfg;
|
return oidcCfg;
|
||||||
@@ -683,7 +771,9 @@ export const oidcConfigServiceFactory = ({
|
|||||||
firstName: claims.given_name ?? "",
|
firstName: claims.given_name ?? "",
|
||||||
lastName: claims.family_name ?? "",
|
lastName: claims.family_name ?? "",
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
callbackPort
|
groups: claims.groups as string[] | undefined,
|
||||||
|
callbackPort,
|
||||||
|
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
||||||
})
|
})
|
||||||
.then(({ isUserCompleted, providerAuthToken }) => {
|
.then(({ isUserCompleted, providerAuthToken }) => {
|
||||||
cb(null, { isUserCompleted, providerAuthToken });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
@@ -697,5 +787,16 @@ export const oidcConfigServiceFactory = ({
|
|||||||
return strategy;
|
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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export type TOidcLoginDTO = {
|
|||||||
lastName?: string;
|
lastName?: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
callbackPort?: string;
|
callbackPort?: string;
|
||||||
|
groups?: string[];
|
||||||
|
manageGroupMemberships?: boolean | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetOidcCfgDTO =
|
export type TGetOidcCfgDTO =
|
||||||
@@ -37,6 +39,7 @@ export type TCreateOidcCfgDTO = {
|
|||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
orgSlug: string;
|
orgSlug: string;
|
||||||
|
manageGroupMemberships: boolean;
|
||||||
} & TGenericPermission;
|
} & TGenericPermission;
|
||||||
|
|
||||||
export type TUpdateOidcCfgDTO = Partial<{
|
export type TUpdateOidcCfgDTO = Partial<{
|
||||||
@@ -52,5 +55,6 @@ export type TUpdateOidcCfgDTO = Partial<{
|
|||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
orgSlug: string;
|
orgSlug: string;
|
||||||
|
manageGroupMemberships: boolean;
|
||||||
}> &
|
}> &
|
||||||
TGenericPermission;
|
TGenericPermission;
|
||||||
|
|||||||
@@ -467,7 +467,8 @@ export const registerRoutes = async (
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
oidcConfigDAL
|
||||||
});
|
});
|
||||||
const groupProjectService = groupProjectServiceFactory({
|
const groupProjectService = groupProjectServiceFactory({
|
||||||
groupDAL,
|
groupDAL,
|
||||||
@@ -1337,7 +1338,13 @@ export const registerRoutes = async (
|
|||||||
smtpService,
|
smtpService,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
oidcConfigDAL
|
oidcConfigDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
groupDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const userEngagementService = userEngagementServiceFactory({
|
const userEngagementService = userEngagementServiceFactory({
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Keycloak OIDC"
|
|
||||||
description: "Learn how to configure Keycloak OIDC for Infisical SSO."
|
|
||||||
---
|
|
||||||
|
|
||||||
<Info>
|
|
||||||
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.
|
|
||||||
</Info>
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step title="Create an OIDC client application in Keycloak">
|
|
||||||
1.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<Info>
|
|
||||||
You don’t typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm.
|
|
||||||
</Info>
|
|
||||||
|
|
||||||
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**.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||

|
|
||||||
<Info>
|
|
||||||
If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com (base URL) with your own domain.
|
|
||||||
</Info>
|
|
||||||
|
|
||||||
1.5. Next, navigate to the **Client scopes** tab and select the client's dedicated scope.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
1.6. Next, click **Add predefined mapper**.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
1.7. Select the **email**, **given name**, **family name** attributes and click **Add**.
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
Once you've completed the above steps, the list of mappers should look like the following:
|
|
||||||

|
|
||||||
|
|
||||||
</Step>
|
|
||||||
<Step title="Retrieve Identity Provider (IdP) Information from Keycloak">
|
|
||||||
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`.
|
|
||||||

|
|
||||||
|
|
||||||
2.2. From the Clients page, navigate to the Credential tab and copy the **Client Secret** to be used in the next steps.
|
|
||||||

|
|
||||||
|
|
||||||
</Step>
|
|
||||||
<Step title="Finish configuring OIDC in Infisical">
|
|
||||||
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect.
|
|
||||||

|
|
||||||
|
|
||||||
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**.
|
|
||||||

|
|
||||||
|
|
||||||
Once you've done that, press **Update** to complete the required configuration.
|
|
||||||
|
|
||||||
</Step>
|
|
||||||
<Step title="Enable OIDC SSO in Infisical">
|
|
||||||
Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
</Step>
|
|
||||||
<Step title="Enforce OIDC SSO in Infisical">
|
|
||||||
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.
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
We recommend ensuring that your account is provisioned using the application in Keycloak
|
|
||||||
prior to enforcing OIDC SSO to prevent any unintended issues.
|
|
||||||
</Warning>
|
|
||||||
|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
<Tip>
|
|
||||||
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.
|
|
||||||
</Tip>
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
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:
|
|
||||||
<div class="height:1px;"/>
|
|
||||||
- `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`.
|
|
||||||
<div class="height:1px;"/>
|
|
||||||
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
|
|
||||||
</Note>
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
When enabled, manual
|
||||||
|
management of Infisical group memberships will be disabled.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Group membership changes in the Keycloak only sync with Infisical when a
|
||||||
|
user logs in. For example, if you remove a user from a group in Keycloak, this change will not be reflected in Infisical until their next login.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Configure a group membership mapper in Keycloak">
|
||||||
|
1.1. In your realm, navigate to the **Clients** tab and select your Infisical client.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1.2. Select the **Client Scopes** tab.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1.3. Next, select the dedicated scope for your Infisical client.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1.4. Click on the **Add mapper** button, and select the **By configuration** option.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1.5. Select the **Group Membership** option.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Setup groups in Infisical and enable OIDC Group Membership Mapping">
|
||||||
|
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.
|
||||||
|

|
||||||
|
|
||||||
|
2.2. Next, enable **OIDC Group Membership Mapping** in Organization Settings > Security.
|
||||||
|

|
||||||
|
|
||||||
|
2.3. The next time a user logs in they will be synced to their matching Keycloak groups.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
113
docs/documentation/platform/sso/keycloak-oidc/overview.mdx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
---
|
||||||
|
title: "Keycloak OIDC Overview"
|
||||||
|
sidebarTitle: "Overview"
|
||||||
|
description: "Learn how to configure Keycloak OIDC for Infisical SSO."
|
||||||
|
---
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
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.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create an OIDC client application in Keycloak">
|
||||||
|
1.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Info>
|
||||||
|
You don’t typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
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**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|

|
||||||
|
<Info>
|
||||||
|
If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com (base URL) with your own domain.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
1.5. Next, navigate to the **Client scopes** tab and select the client's dedicated scope.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1.6. Next, click **Add predefined mapper**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1.7. Select the **email**, **given name**, **family name** attributes and click **Add**.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Once you've completed the above steps, the list of mappers should look like the following:
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Retrieve Identity Provider (IdP) Information from Keycloak">
|
||||||
|
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`.
|
||||||
|

|
||||||
|
|
||||||
|
2.2. From the Clients page, navigate to the Credential tab and copy the **Client Secret** to be used in the next steps.
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Finish configuring OIDC in Infisical">
|
||||||
|
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect.
|
||||||
|

|
||||||
|
|
||||||
|
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**.
|
||||||
|

|
||||||
|
|
||||||
|
Once you've done that, press **Update** to complete the required configuration.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Enable OIDC SSO in Infisical">
|
||||||
|
Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Enforce OIDC SSO in Infisical">
|
||||||
|
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.
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
We recommend ensuring that your account is provisioned using the application in Keycloak
|
||||||
|
prior to enforcing OIDC SSO to prevent any unintended issues.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
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.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
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:
|
||||||
|
<div class="height:1px;"/>
|
||||||
|
- `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`.
|
||||||
|
<div class="height:1px;"/>
|
||||||
|
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
|
||||||
|
</Note>
|
||||||
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 396 KiB |
|
After Width: | Height: | Size: 468 KiB |
|
After Width: | Height: | Size: 578 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -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` |
|
| `pki-collections` | `read`, `create`, `edit`, `delete` |
|
||||||
| `kms` | `edit` |
|
| `kms` | `edit` |
|
||||||
| `cmek` | `read`, `create`, `edit`, `delete`, `encrypt`, `decrypt` |
|
| `cmek` | `read`, `create`, `edit`, `delete`, `encrypt`, `decrypt` |
|
||||||
|
| `secret-syncs` | `read`, `create`, `edit`, `delete`, `sync-secrets`, `import-secrets`, `remove-secrets` |
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
@@ -63,21 +64,23 @@ Refer to the table below for a list of subjects and the actions they support.
|
|||||||
`read`, `create`, `edit`, `delete`.
|
`read`, `create`, `edit`, `delete`.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
| Subject | Actions |
|
| Subject | Actions |
|
||||||
| ------------------ | ---------------------------------- |
|
| --------------------- | ------------------------------------------------ |
|
||||||
| `workspace` | `read`, `create` |
|
| `workspace` | `read`, `create` |
|
||||||
| `role` | `read`, `create`, `edit`, `delete` |
|
| `role` | `read`, `create`, `edit`, `delete` |
|
||||||
| `member` | `read`, `create`, `edit`, `delete` |
|
| `member` | `read`, `create`, `edit`, `delete` |
|
||||||
| `secret-scanning` | `read`, `create`, `edit`, `delete` |
|
| `secret-scanning` | `read`, `create`, `edit`, `delete` |
|
||||||
| `settings` | `read`, `create`, `edit`, `delete` |
|
| `settings` | `read`, `create`, `edit`, `delete` |
|
||||||
| `incident-account` | `read`, `create`, `edit`, `delete` |
|
| `incident-account` | `read`, `create`, `edit`, `delete` |
|
||||||
| `sso` | `read`, `create`, `edit`, `delete` |
|
| `sso` | `read`, `create`, `edit`, `delete` |
|
||||||
| `scim` | `read`, `create`, `edit`, `delete` |
|
| `scim` | `read`, `create`, `edit`, `delete` |
|
||||||
| `ldap` | `read`, `create`, `edit`, `delete` |
|
| `ldap` | `read`, `create`, `edit`, `delete` |
|
||||||
| `groups` | `read`, `create`, `edit`, `delete` |
|
| `groups` | `read`, `create`, `edit`, `delete` |
|
||||||
| `billing` | `read`, `create`, `edit`, `delete` |
|
| `billing` | `read`, `create`, `edit`, `delete` |
|
||||||
| `identity` | `read`, `create`, `edit`, `delete` |
|
| `identity` | `read`, `create`, `edit`, `delete` |
|
||||||
| `kms` | `read` |
|
| `project-templates` | `read`, `create`, `edit`, `delete` |
|
||||||
|
| `app-connections` | `read`, `create`, `edit`, `delete`, `connect` |
|
||||||
|
| `kms` | `read` |
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -249,7 +249,13 @@
|
|||||||
"documentation/platform/sso/keycloak-saml",
|
"documentation/platform/sso/keycloak-saml",
|
||||||
"documentation/platform/sso/google-saml",
|
"documentation/platform/sso/google-saml",
|
||||||
"documentation/platform/sso/auth0-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/auth0-oidc",
|
||||||
"documentation/platform/sso/general-oidc"
|
"documentation/platform/sso/general-oidc"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { EFilterReturnedUsers, TGroup, TGroupUser } from "./types";
|
|||||||
|
|
||||||
export const groupKeys = {
|
export const groupKeys = {
|
||||||
getGroupById: (groupId: string) => [{ groupId }, "group"] as const,
|
getGroupById: (groupId: string) => [{ groupId }, "group"] as const,
|
||||||
|
isGroupMembershipManagementDisabled: (orgId: string) =>
|
||||||
|
["group-memberships-management-disabled", orgId] as const,
|
||||||
allGroupUserMemberships: () => ["group-user-memberships"] as const,
|
allGroupUserMemberships: () => ["group-user-memberships"] as const,
|
||||||
forGroupUserMemberships: (slug: string) =>
|
forGroupUserMemberships: (slug: string) =>
|
||||||
[...groupKeys.allGroupUserMemberships(), slug] as const,
|
[...groupKeys.allGroupUserMemberships(), slug] as const,
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ export const useUpdateOIDCConfig = () => {
|
|||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
isActive,
|
isActive,
|
||||||
orgSlug
|
orgSlug,
|
||||||
|
manageGroupMemberships
|
||||||
}: {
|
}: {
|
||||||
allowedEmailDomains?: string;
|
allowedEmailDomains?: string;
|
||||||
issuer?: string;
|
issuer?: string;
|
||||||
@@ -34,6 +35,7 @@ export const useUpdateOIDCConfig = () => {
|
|||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
configurationType?: string;
|
configurationType?: string;
|
||||||
orgSlug: string;
|
orgSlug: string;
|
||||||
|
manageGroupMemberships?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.patch("/api/v1/sso/oidc/config", {
|
const { data } = await apiRequest.patch("/api/v1/sso/oidc/config", {
|
||||||
issuer,
|
issuer,
|
||||||
@@ -47,7 +49,8 @@ export const useUpdateOIDCConfig = () => {
|
|||||||
clientId,
|
clientId,
|
||||||
orgSlug,
|
orgSlug,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
isActive
|
isActive,
|
||||||
|
manageGroupMemberships
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -74,7 +77,8 @@ export const useCreateOIDCConfig = () => {
|
|||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
isActive,
|
isActive,
|
||||||
orgSlug
|
orgSlug,
|
||||||
|
manageGroupMemberships
|
||||||
}: {
|
}: {
|
||||||
issuer?: string;
|
issuer?: string;
|
||||||
configurationType: string;
|
configurationType: string;
|
||||||
@@ -88,6 +92,7 @@ export const useCreateOIDCConfig = () => {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
orgSlug: string;
|
orgSlug: string;
|
||||||
allowedEmailDomains?: string;
|
allowedEmailDomains?: string;
|
||||||
|
manageGroupMemberships?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.post("/api/v1/sso/oidc/config", {
|
const { data } = await apiRequest.post("/api/v1/sso/oidc/config", {
|
||||||
issuer,
|
issuer,
|
||||||
@@ -101,7 +106,8 @@ export const useCreateOIDCConfig = () => {
|
|||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
isActive,
|
isActive,
|
||||||
orgSlug
|
orgSlug,
|
||||||
|
manageGroupMemberships
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ 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: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const,
|
||||||
|
getOIDCManageGroupMembershipsEnabled: (orgId: string) =>
|
||||||
|
["oidc-manage-group-memberships", orgId] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetOIDCConfig = (orgSlug: string) => {
|
export const useGetOIDCConfig = (orgSlug: string) => {
|
||||||
@@ -25,3 +27,16 @@ export const useGetOIDCConfig = (orgSlug: string) => {
|
|||||||
enabled: true
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ export type OIDCConfigData = {
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
allowedEmailDomains?: string;
|
allowedEmailDomains?: string;
|
||||||
|
manageGroupMemberships: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -82,9 +82,9 @@ export const AddGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
text: "Successfully assigned user to the group",
|
text: "Successfully assigned user to the group",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (error) {
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to assign user to the group",
|
text: (error as Error)?.message ?? "Failed to assign user to the group",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { DeleteActionModal, IconButton } from "@app/components/v2";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { useRemoveUserFromGroup } from "@app/hooks/api";
|
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 { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { AddGroupMembersModal } from "../AddGroupMemberModal";
|
import { AddGroupMembersModal } from "../AddGroupMemberModal";
|
||||||
@@ -20,6 +22,11 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {
|
|||||||
"removeMemberFromGroup"
|
"removeMemberFromGroup"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
|
const { data: isOidcManageGroupMembershipsEnabled = false } =
|
||||||
|
useOidcManageGroupMembershipsEnabled(currentOrg.id);
|
||||||
|
|
||||||
const { mutateAsync: removeUserFromGroupMutateAsync } = useRemoveUserFromGroup();
|
const { mutateAsync: removeUserFromGroupMutateAsync } = useRemoveUserFromGroup();
|
||||||
const handleRemoveUserFromGroup = async (username: string) => {
|
const handleRemoveUserFromGroup = async (username: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -47,19 +54,35 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {
|
|||||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
<h3 className="text-lg font-semibold text-mineshaft-100">Group Members</h3>
|
<h3 className="text-lg font-semibold text-mineshaft-100">Group Members</h3>
|
||||||
<IconButton
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||||
ariaLabel="copy icon"
|
{(isAllowed) => (
|
||||||
variant="plain"
|
<Tooltip
|
||||||
className="group relative"
|
className="text-center"
|
||||||
onClick={() => {
|
content={
|
||||||
handlePopUpOpen("addGroupMembers", {
|
isOidcManageGroupMembershipsEnabled
|
||||||
groupId,
|
? "OIDC Group Membership Mapping Enabled. Disable to manually manage user groups."
|
||||||
slug: groupSlug
|
: undefined
|
||||||
});
|
}
|
||||||
}}
|
>
|
||||||
>
|
<div className="mb-4 flex items-center justify-center">
|
||||||
<FontAwesomeIcon icon={faPlus} />
|
<IconButton
|
||||||
</IconButton>
|
isDisabled={isOidcManageGroupMembershipsEnabled || !isAllowed}
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("addGroupMembers", {
|
||||||
|
groupId,
|
||||||
|
slug: groupSlug
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<GroupMembersTable
|
<GroupMembersTable
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ import {
|
|||||||
TBody,
|
TBody,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
|
Tooltip,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||||
import { useListGroupUsers } from "@app/hooks/api";
|
import { useListGroupUsers, useOidcManageGroupMembershipsEnabled } from "@app/hooks/api";
|
||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { EFilterReturnedUsers } from "@app/hooks/api/groups/types";
|
import { EFilterReturnedUsers } from "@app/hooks/api/groups/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
@@ -58,6 +59,11 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
|
|||||||
toggleOrderDirection
|
toggleOrderDirection
|
||||||
} = usePagination(GroupMembersOrderBy.Name, { initPerPage: 10 });
|
} = usePagination(GroupMembersOrderBy.Name, { initPerPage: 10 });
|
||||||
|
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
|
const { data: isOidcManageGroupMembershipsEnabled = false } =
|
||||||
|
useOidcManageGroupMembershipsEnabled(currentOrg.id);
|
||||||
|
|
||||||
const { data: groupMemberships, isPending } = useListGroupUsers({
|
const { data: groupMemberships, isPending } = useListGroupUsers({
|
||||||
id: groupId,
|
id: groupId,
|
||||||
groupSlug,
|
groupSlug,
|
||||||
@@ -173,19 +179,30 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
|
|||||||
{!groupMemberships?.users.length && (
|
{!groupMemberships?.users.length && (
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<div className="mb-4 flex items-center justify-center">
|
<Tooltip
|
||||||
<Button
|
className="text-center"
|
||||||
isDisabled={!isAllowed}
|
content={
|
||||||
onClick={() => {
|
isOidcManageGroupMembershipsEnabled
|
||||||
handlePopUpOpen("addGroupMembers", {
|
? "OIDC Group Membership Mapping Enabled. Disable to manually manage user groups."
|
||||||
groupId,
|
: undefined
|
||||||
slug: groupSlug
|
}
|
||||||
});
|
>
|
||||||
}}
|
<div className="mb-4 flex items-center justify-center">
|
||||||
>
|
<Button
|
||||||
Add members
|
variant="solid"
|
||||||
</Button>
|
colorSchema="secondary"
|
||||||
</div>
|
isDisabled={isOidcManageGroupMembershipsEnabled || !isAllowed}
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("addGroupMembers", {
|
||||||
|
groupId,
|
||||||
|
slug: groupSlug
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add members
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
|
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 { TGroupUser } from "@app/hooks/api/groups/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
@@ -19,6 +20,11 @@ export const GroupMembershipRow = ({
|
|||||||
user: { firstName, lastName, username, joinedGroupAt, email, id },
|
user: { firstName, lastName, username, joinedGroupAt, email, id },
|
||||||
handlePopUpOpen
|
handlePopUpOpen
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
|
const { data: isOidcManageGroupMembershipsEnabled = false } =
|
||||||
|
useOidcManageGroupMembershipsEnabled(currentOrg.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr className="items-center" key={`group-user-${id}`}>
|
<Tr className="items-center" key={`group-user-${id}`}>
|
||||||
<Td>
|
<Td>
|
||||||
@@ -36,15 +42,21 @@ export const GroupMembershipRow = ({
|
|||||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||||
{(isAllowed) => {
|
{(isAllowed) => {
|
||||||
return (
|
return (
|
||||||
<Tooltip content="Remove user from group">
|
<Tooltip
|
||||||
|
content={
|
||||||
|
isOidcManageGroupMembershipsEnabled
|
||||||
|
? "OIDC Group Membership Mapping Enabled. Disable to manually manage user groups."
|
||||||
|
: "Remove user from group"
|
||||||
|
}
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled}
|
||||||
ariaLabel="Remove user from group"
|
ariaLabel="Remove user from group"
|
||||||
onClick={() => handlePopUpOpen("removeMemberFromGroup", { username })}
|
onClick={() => handlePopUpOpen("removeMemberFromGroup", { username })}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faUserMinus} className="cursor-pointer" />
|
<FontAwesomeIcon icon={faUserMinus} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, Switch } from "@app/components/v2";
|
import { Button, Switch, Tooltip } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects,
|
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 () => {
|
const addOidcButtonClick = async () => {
|
||||||
if (subscription?.oidcSSO && currentOrg) {
|
if (subscription?.oidcSSO && currentOrg) {
|
||||||
handlePopUpOpen("addOIDC");
|
handlePopUpOpen("addOIDC");
|
||||||
@@ -148,6 +174,63 @@ export const OrgOIDCSection = (): JSX.Element => {
|
|||||||
Enforce members to authenticate via OIDC to access this organization
|
Enforce members to authenticate via OIDC to access this organization
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="py-4">
|
||||||
|
<div className="mb-2 flex justify-between">
|
||||||
|
<div className="text-md flex items-center text-mineshaft-100">
|
||||||
|
<span>OIDC Group Membership Mapping</span>
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-lg"
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p className="mt-4">
|
||||||
|
To use this feature you must include group claims in the OIDC token.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline underline-offset-2 hover:text-mineshaft-300"
|
||||||
|
href="https://infisical.com/docs/documentation/platform/sso/overview"
|
||||||
|
>
|
||||||
|
See your OIDC provider docs for details.
|
||||||
|
</a>
|
||||||
|
<p className="mt-4 text-yellow">
|
||||||
|
<FontAwesomeIcon className="mr-1" icon={faWarning} />
|
||||||
|
Group membership changes in the OIDC provider only sync with Infisical when a
|
||||||
|
user logs in. 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 login.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faInfoCircle}
|
||||||
|
size="sm"
|
||||||
|
className="ml-1 mt-0.5 inline-block text-mineshaft-400"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Switch
|
||||||
|
id="enforce-org-auth"
|
||||||
|
isChecked={data?.manageGroupMemberships ?? false}
|
||||||
|
onCheckedChange={(value) => handleOIDCGroupManagement(value)}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-mineshaft-300">
|
||||||
|
Infisical will manage user group memberships based on the OIDC provider
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<OIDCModal
|
<OIDCModal
|
||||||
popUp={popUp}
|
popUp={popUp}
|
||||||
handlePopUpClose={handlePopUpClose}
|
handlePopUpClose={handlePopUpClose}
|
||||||
|
|||||||