From f21bbb26d514d5d4440befe33d13632c84afaea4 Mon Sep 17 00:00:00 2001 From: Agnieszka Gancarczyk <4890675+agagancarczyk@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:20:42 +0000 Subject: [PATCH] Introduced Permissions tab (#35409) * Introduced Permissions tab Signed-off-by: Agnieszka Gancarczyk --- .../admin/messages/messages_en.properties | 3 +- js/apps/admin-ui/src/PageNav.tsx | 4 + .../admin-ui/src/clients/ClientDetails.tsx | 4 + js/apps/admin-ui/src/index.ts | 1 + .../src/permissions/PermissionsSection.tsx | 223 ++++++++++++++++++ js/apps/admin-ui/src/permissions/routes.ts | 7 + .../src/permissions/routes/Permissions.tsx | 21 ++ .../permissions/routes/PermissionsTabs.tsx | 28 +++ js/apps/admin-ui/src/routes.tsx | 2 + .../src/utils/useIsAdminPermissionsClient.ts | 29 +++ .../src/defs/realmRepresentation.ts | 1 + 11 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 js/apps/admin-ui/src/permissions/PermissionsSection.tsx create mode 100644 js/apps/admin-ui/src/permissions/routes.ts create mode 100644 js/apps/admin-ui/src/permissions/routes/Permissions.tsx create mode 100644 js/apps/admin-ui/src/permissions/routes/PermissionsTabs.tsx create mode 100644 js/apps/admin-ui/src/utils/useIsAdminPermissionsClient.ts diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index c56703517a8..b5a93cb8501 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -3332,4 +3332,5 @@ UNMANAGED=Unmanaged deleteConfirmUsers_one=Delete user {{name}}? deleteConfirmUsers_other=Delete {{count}} users? downloadThemeJar=Download theme JAR -themeColorInfo=Here you can set the patternfly color variables and create a "theme jar" file that you can download and put in your providers folder to apply the theme to your realm. \ No newline at end of file +themeColorInfo=Here you can set the patternfly color variables and create a "theme jar" file that you can download and put in your providers folder to apply the theme to your realm. +permissionsSubTitle=Fine-grained admin permissions allow assigning detailed, specific access rights, controlling which resources and actions can be managed. diff --git a/js/apps/admin-ui/src/PageNav.tsx b/js/apps/admin-ui/src/PageNav.tsx index 279f422cc3e..738a5643324 100644 --- a/js/apps/admin-ui/src/PageNav.tsx +++ b/js/apps/admin-ui/src/PageNav.tsx @@ -128,6 +128,10 @@ export const PageNav = () => { + {isFeatureEnabled(Feature.AdminFineGrainedAuthzV2) && + realmRepresentation?.adminPermissionsEnabled && ( + + )} {isFeatureEnabled(Feature.DeclarativeUI) && pages?.map((p) => ( void; @@ -213,6 +214,8 @@ export default function ClientDetails() { const { clientId } = useParams(); const [key, setKey] = useState(0); + const isAdminPermissionsClient = useIsAdminPermissionsClient(clientId); + const clientAuthenticatorType = useWatch({ control: form.control, name: "clientAuthenticatorType", @@ -547,6 +550,7 @@ export default function ClientDetails() { )} {client!.authorizationServicesEnabled && + !isAdminPermissionsClient && (hasManageAuthorization || hasViewAuthorization) && ( (); + const [changeAuthenticatorOpen, toggleChangeAuthenticatorOpen] = useToggle(); + const form = useForm(); + + const usePermissionsTabs = (tab: PermissionsTabs) => + useRoutableTab( + toPermissionsTabs({ + realm, + tab, + }), + ); + + const clientAuthenticatorType = useWatch({ + control: form.control, + name: "clientAuthenticatorType", + defaultValue: "client-secret", + }); + + const hasManageAuthorization = hasAccess("manage-authorization"); + const hasViewUsers = hasAccess("view-users"); + const permissionsResourcesTab = usePermissionsTabs("resources"); + const permissionsPoliciesTab = usePermissionsTabs("policies"); + const permissionsEvaluateTab = usePermissionsTabs("evaluate"); + + useFetch( + async () => { + const clients = await adminClient.clients.find(); + return clients; + }, + (clients) => { + const adminPermissionsClient = clients.find( + (client) => client.clientId === "admin-permissions", + ); + setAdminPermissionsClient(adminPermissionsClient!); + }, + [], + ); + + const setupForm = (client: ClientRepresentation) => { + form.reset({ ...client }); + convertToFormValues(client, form.setValue); + if (client.attributes?.["acr.loa.map"]) { + form.setValue( + convertAttributeNameToForm("attributes.acr.loa.map"), + // @ts-ignore + Object.entries(JSON.parse(client.attributes["acr.loa.map"])).flatMap( + ([key, value]) => ({ key, value }), + ), + ); + } + }; + + const save = async ( + { confirmed = false, messageKey = "clientSaveSuccess" }: SaveOptions = { + confirmed: false, + messageKey: "clientSaveSuccess", + }, + ) => { + if (!(await form.trigger())) { + return; + } + + if ( + !adminPermissionsClient?.publicClient && + adminPermissionsClient?.clientAuthenticatorType !== + clientAuthenticatorType && + !confirmed + ) { + toggleChangeAuthenticatorOpen(); + return; + } + + const values = convertFormValuesToObject(form.getValues()); + + const submittedClient = + convertFormValuesToObject(values); + + if (submittedClient.attributes?.["acr.loa.map"]) { + submittedClient.attributes["acr.loa.map"] = JSON.stringify( + Object.fromEntries( + (submittedClient.attributes["acr.loa.map"] as KeyValueType[]) + .filter(({ key }) => key !== "") + .map(({ key, value }) => [key, value]), + ), + ); + } + + try { + const newClient: ClientRepresentation = { + ...adminPermissionsClient, + ...submittedClient, + }; + + newClient.clientId = newClient.clientId?.trim(); + + await adminClient.clients.update( + { id: adminPermissionsClient!.clientId! }, + newClient, + ); + setupForm(newClient); + setAdminPermissionsClient(newClient); + addAlert(t(messageKey), AlertVariant.success); + } catch (error) { + addError("clientSaveError", error); + } + }; + + return ( + adminPermissionsClient && ( + <> + save({ confirmed: true })} + > + <> + {t("changeAuthenticatorConfirm", { + clientAuthenticatorType: clientAuthenticatorType, + })} + + + + + + + {t("resources")}} + {...permissionsResourcesTab} + > + + + {t("policies")}} + {...permissionsPoliciesTab} + > + + + {hasViewUsers && ( + {t("evaluate")}} + {...permissionsEvaluateTab} + > + + + )} + + + + + ) + ); +} diff --git a/js/apps/admin-ui/src/permissions/routes.ts b/js/apps/admin-ui/src/permissions/routes.ts new file mode 100644 index 00000000000..4c713caff37 --- /dev/null +++ b/js/apps/admin-ui/src/permissions/routes.ts @@ -0,0 +1,7 @@ +import type { AppRouteObject } from "../routes"; +import { PermissionsRoute } from "./routes/Permissions"; +import { PermissionsTabsRoute } from "./routes/PermissionsTabs"; + +const routes: AppRouteObject[] = [PermissionsRoute, PermissionsTabsRoute]; + +export default routes; diff --git a/js/apps/admin-ui/src/permissions/routes/Permissions.tsx b/js/apps/admin-ui/src/permissions/routes/Permissions.tsx new file mode 100644 index 00000000000..67af3b624ff --- /dev/null +++ b/js/apps/admin-ui/src/permissions/routes/Permissions.tsx @@ -0,0 +1,21 @@ +import { lazy } from "react"; +import type { Path } from "react-router-dom"; +import { generateEncodedPath } from "../../utils/generateEncodedPath"; +import type { AppRouteObject } from "../../routes"; + +export type PermissionsParams = { realm: string }; + +const PermissionsSection = lazy(() => import("../PermissionsSection")); + +export const PermissionsRoute: AppRouteObject = { + path: "/:realm/permissions", + element: , + breadcrumb: (t) => t("titlePermissions"), + handle: { + access: ["view-realm", "view-clients", "view-users"], + }, +}; + +export const toPermissions = (params: PermissionsParams): Partial => ({ + pathname: generateEncodedPath(PermissionsRoute.path, params), +}); diff --git a/js/apps/admin-ui/src/permissions/routes/PermissionsTabs.tsx b/js/apps/admin-ui/src/permissions/routes/PermissionsTabs.tsx new file mode 100644 index 00000000000..ac1c0aabff3 --- /dev/null +++ b/js/apps/admin-ui/src/permissions/routes/PermissionsTabs.tsx @@ -0,0 +1,28 @@ +import { lazy } from "react"; +import type { Path } from "react-router-dom"; +import { generateEncodedPath } from "../../utils/generateEncodedPath"; +import type { AppRouteObject } from "../../routes"; + +export type PermissionsTabs = "resources" | "policies" | "evaluate"; + +export type PermissionsTabsParams = { + realm: string; + tab: PermissionsTabs; +}; + +const PermissionsSection = lazy(() => import("../PermissionsSection")); + +export const PermissionsTabsRoute: AppRouteObject = { + path: "/:realm/permissions/:tab", + element: , + handle: { + access: (accessChecker) => + accessChecker.hasAny("view-realm", "view-clients", "view-users"), + }, +}; + +export const toPermissionsTabs = ( + params: PermissionsTabsParams, +): Partial => ({ + pathname: generateEncodedPath(PermissionsTabsRoute.path, params), +}); diff --git a/js/apps/admin-ui/src/routes.tsx b/js/apps/admin-ui/src/routes.tsx index 74d4d6f7125..bf8763cd793 100644 --- a/js/apps/admin-ui/src/routes.tsx +++ b/js/apps/admin-ui/src/routes.tsx @@ -19,6 +19,7 @@ import realmRoutes from "./realm/routes"; import sessionRoutes from "./sessions/routes"; import userFederationRoutes from "./user-federation/routes"; import userRoutes from "./user/routes"; +import permissionsRoute from "./permissions/routes"; export type AppRouteObjectHandle = { access: AccessType | AccessType[]; @@ -50,6 +51,7 @@ export const routes: AppRouteObject[] = [ ...realmSettingRoutes, ...sessionRoutes, ...userFederationRoutes, + ...permissionsRoute, ...userRoutes, ...groupsRoutes, ...dashboardRoutes, diff --git a/js/apps/admin-ui/src/utils/useIsAdminPermissionsClient.ts b/js/apps/admin-ui/src/utils/useIsAdminPermissionsClient.ts new file mode 100644 index 00000000000..31095924876 --- /dev/null +++ b/js/apps/admin-ui/src/utils/useIsAdminPermissionsClient.ts @@ -0,0 +1,29 @@ +import { useState } from "react"; +import { useFetch } from "@keycloak/keycloak-ui-shared"; +import { useAdminClient } from "../admin-client"; +import ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation"; + +export function useIsAdminPermissionsClient(selectedClientId: string) { + const { adminClient } = useAdminClient(); + const [isAdminPermissionsClient, setIsAdminPermissionsClient] = + useState(false); + + useFetch( + async () => { + const clients: ClientRepresentation[] = await adminClient.clients.find(); + return clients; + }, + (clients: ClientRepresentation[]) => { + const adminPermissionsClient = clients.find( + (client) => client.clientId === "admin-permissions", + ); + + setIsAdminPermissionsClient( + selectedClientId === adminPermissionsClient?.id, + ); + }, + [], + ); + + return isAdminPermissionsClient; +} diff --git a/js/libs/keycloak-admin-client/src/defs/realmRepresentation.ts b/js/libs/keycloak-admin-client/src/defs/realmRepresentation.ts index 93d87bdc867..4600a3d0ccc 100644 --- a/js/libs/keycloak-admin-client/src/defs/realmRepresentation.ts +++ b/js/libs/keycloak-admin-client/src/defs/realmRepresentation.ts @@ -24,6 +24,7 @@ export default interface RealmRepresentation { actionTokenGeneratedByUserLifespan?: number; adminEventsDetailsEnabled?: boolean; adminEventsEnabled?: boolean; + adminPermissionsEnabled?: boolean; adminTheme?: string; attributes?: Record; // AuthenticationFlowRepresentation