mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 00:52:09 +00:00
feature: oidc group membership mapping
This commit is contained in:
@@ -6,6 +6,8 @@ import { EFilterReturnedUsers, TGroup, TGroupUser } from "./types";
|
||||
|
||||
export const groupKeys = {
|
||||
getGroupById: (groupId: string) => [{ groupId }, "group"] as const,
|
||||
isGroupMembershipManagementDisabled: (orgId: string) =>
|
||||
["group-memberships-management-disabled", orgId] as const,
|
||||
allGroupUserMemberships: () => ["group-user-memberships"] as const,
|
||||
forGroupUserMemberships: (slug: string) =>
|
||||
[...groupKeys.allGroupUserMemberships(), slug] as const,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -12,4 +12,5 @@ export type OIDCConfigData = {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
allowedEmailDomains?: string;
|
||||
manageGroupMemberships: boolean;
|
||||
};
|
||||
|
||||
+2
-2
@@ -82,9 +82,9 @@ export const AddGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
text: "Successfully assigned user to the group",
|
||||
type: "success"
|
||||
});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
createNotification({
|
||||
text: "Failed to assign user to the group",
|
||||
text: (error as Error)?.message ?? "Failed to assign user to the group",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
|
||||
+38
-15
@@ -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) => {
|
||||
<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">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Group Members</h3>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addGroupMembers", {
|
||||
groupId,
|
||||
slug: groupSlug
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => (
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={
|
||||
isOidcManageGroupMembershipsEnabled
|
||||
? "OIDC Group Membership Mapping Enabled. Disable to manually manage user groups."
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-center">
|
||||
<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 className="py-4">
|
||||
<GroupMembersTable
|
||||
|
||||
+32
-15
@@ -21,11 +21,12 @@ import {
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
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 { EFilterReturnedUsers } from "@app/hooks/api/groups/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
@@ -58,6 +59,11 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
|
||||
toggleOrderDirection
|
||||
} = usePagination(GroupMembersOrderBy.Name, { initPerPage: 10 });
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const { data: isOidcManageGroupMembershipsEnabled = false } =
|
||||
useOidcManageGroupMembershipsEnabled(currentOrg.id);
|
||||
|
||||
const { data: groupMemberships, isPending } = useListGroupUsers({
|
||||
id: groupId,
|
||||
groupSlug,
|
||||
@@ -173,19 +179,30 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
|
||||
{!groupMemberships?.users.length && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => (
|
||||
<div className="mb-4 flex items-center justify-center">
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addGroupMembers", {
|
||||
groupId,
|
||||
slug: groupSlug
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add members
|
||||
</Button>
|
||||
</div>
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={
|
||||
isOidcManageGroupMembershipsEnabled
|
||||
? "OIDC Group Membership Mapping Enabled. Disable to manually manage user groups."
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-center">
|
||||
<Button
|
||||
variant="solid"
|
||||
colorSchema="secondary"
|
||||
isDisabled={isOidcManageGroupMembershipsEnabled || !isAllowed}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addGroupMembers", {
|
||||
groupId,
|
||||
slug: groupSlug
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add members
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
|
||||
+16
-4
@@ -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 (
|
||||
<Tr className="items-center" key={`group-user-${id}`}>
|
||||
<Td>
|
||||
@@ -36,15 +42,21 @@ export const GroupMembershipRow = ({
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => {
|
||||
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
|
||||
isDisabled={!isAllowed}
|
||||
isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled}
|
||||
ariaLabel="Remove user from group"
|
||||
onClick={() => handlePopUpOpen("removeMemberFromGroup", { username })}
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
>
|
||||
<FontAwesomeIcon icon={faUserMinus} className="cursor-pointer" />
|
||||
<FontAwesomeIcon icon={faUserMinus} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
+84
-1
@@ -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,63 @@ export const OrgOIDCSection = (): JSX.Element => {
|
||||
Enforce members to authenticate via OIDC to access this organization
|
||||
</p>
|
||||
</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
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
|
||||
Reference in New Issue
Block a user