Introduced Permissions tab (#35409)

* Introduced Permissions tab

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>
This commit is contained in:
Agnieszka Gancarczyk 2024-12-10 11:20:42 +00:00 committed by GitHub
parent 7c4a5aed77
commit f21bbb26d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 322 additions and 1 deletions

View File

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

View File

@ -128,6 +128,10 @@ export const PageNav = () => {
<LeftNav title="authentication" path="/authentication" />
<LeftNav title="identityProviders" path="/identity-providers" />
<LeftNav title="userFederation" path="/user-federation" />
{isFeatureEnabled(Feature.AdminFineGrainedAuthzV2) &&
realmRepresentation?.adminPermissionsEnabled && (
<LeftNav title="permissions" path="/permissions" />
)}
{isFeatureEnabled(Feature.DeclarativeUI) &&
pages?.map((p) => (
<LeftNav

View File

@ -73,6 +73,7 @@ import { EvaluateScopes } from "./scopes/EvaluateScopes";
import { ServiceAccount } from "./service-account/ServiceAccount";
import { getProtocolName, isRealmClient } from "./utils";
import { UserEvents } from "../events/UserEvents";
import { useIsAdminPermissionsClient } from "../utils/useIsAdminPermissionsClient";
type ClientDetailHeaderProps = {
onChange: (value: boolean) => void;
@ -213,6 +214,8 @@ export default function ClientDetails() {
const { clientId } = useParams<ClientParams>();
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() {
</Tab>
)}
{client!.authorizationServicesEnabled &&
!isAdminPermissionsClient &&
(hasManageAuthorization || hasViewAuthorization) && (
<Tab
id="authorization"

View File

@ -314,3 +314,4 @@ export { App as AdminUi } from "./App";
export type { Environment as AccountEnvironment } from "./environment";
export { KeycloakProvider, useEnvironment } from "@keycloak/keycloak-ui-shared";
export { AdminClientContext, initAdminClient } from "./admin-client";
export * as PermissionsSection from "./permissions/PermissionsSection";

View File

@ -0,0 +1,223 @@
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { useAlerts, useFetch } from "@keycloak/keycloak-ui-shared";
import { useState } from "react";
import { useAdminClient } from "../admin-client";
import { ViewHeader } from "../components/view-header/ViewHeader";
import {
RoutableTabs,
useRoutableTab,
} from "../components/routable-tabs/RoutableTabs";
import {
PermissionsTabs,
toPermissionsTabs,
} from "../permissions/routes/PermissionsTabs";
import {
AlertVariant,
PageSection,
Tab,
TabTitleText,
} from "@patternfly/react-core";
import { AuthorizationResources } from "../clients/authorization/Resources";
import { AuthorizationPolicies } from "../clients/authorization/Policies";
import { AuthorizationEvaluate } from "../clients/authorization/AuthorizationEvaluate";
import { useRealm } from "../context/realm-context/RealmContext";
import { useAccess } from "../context/access/Access";
import { useTranslation } from "react-i18next";
import { FormProvider, useForm, useWatch } from "react-hook-form";
import { FormFields, SaveOptions } from "../clients/ClientDetails";
import {
convertAttributeNameToForm,
convertFormValuesToObject,
convertToFormValues,
} from "../util";
import { ConfirmDialogModal } from "../components/confirm-dialog/ConfirmDialog";
import { KeyValueType } from "../components/key-value-form/key-value-convert";
import useToggle from "../utils/useToggle";
export default function PermissionsSection() {
const { adminClient } = useAdminClient();
const { t } = useTranslation();
const { realm } = useRealm();
const { hasAccess } = useAccess();
const { addAlert, addError } = useAlerts();
const [adminPermissionsClient, setAdminPermissionsClient] = useState<
ClientRepresentation | undefined
>();
const [changeAuthenticatorOpen, toggleChangeAuthenticatorOpen] = useToggle();
const form = useForm<FormFields>();
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<ClientRepresentation>(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 && (
<>
<ConfirmDialogModal
continueButtonLabel="yes"
cancelButtonLabel="no"
titleKey={t("changeAuthenticatorConfirmTitle", {
clientAuthenticatorType: clientAuthenticatorType,
})}
open={changeAuthenticatorOpen}
toggleDialog={toggleChangeAuthenticatorOpen}
onConfirm={() => save({ confirmed: true })}
>
<>
{t("changeAuthenticatorConfirm", {
clientAuthenticatorType: clientAuthenticatorType,
})}
</>
</ConfirmDialogModal>
<PageSection variant="light" className="pf-v5-u-p-0">
<FormProvider {...form}>
<ViewHeader
titleKey={t("permissions")}
subKey={t("permissionsSubTitle")}
/>
<RoutableTabs
mountOnEnter
unmountOnExit
defaultLocation={toPermissionsTabs({
realm,
tab: "resources",
})}
>
<Tab
id="resources"
data-testid="permissionsResources"
title={<TabTitleText>{t("resources")}</TabTitleText>}
{...permissionsResourcesTab}
>
<AuthorizationResources clientId={adminPermissionsClient.id!} />
</Tab>
<Tab
id="policies"
data-testid="permissionsPolicies"
title={<TabTitleText>{t("policies")}</TabTitleText>}
{...permissionsPoliciesTab}
>
<AuthorizationPolicies
clientId={adminPermissionsClient.id!}
isDisabled={!hasManageAuthorization}
/>
</Tab>
{hasViewUsers && (
<Tab
id="evaluate"
data-testid="permissionsEvaluate"
title={<TabTitleText>{t("evaluate")}</TabTitleText>}
{...permissionsEvaluateTab}
>
<AuthorizationEvaluate
client={adminPermissionsClient}
save={save}
/>
</Tab>
)}
</RoutableTabs>
</FormProvider>
</PageSection>
</>
)
);
}

View File

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

View File

@ -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: <PermissionsSection />,
breadcrumb: (t) => t("titlePermissions"),
handle: {
access: ["view-realm", "view-clients", "view-users"],
},
};
export const toPermissions = (params: PermissionsParams): Partial<Path> => ({
pathname: generateEncodedPath(PermissionsRoute.path, params),
});

View File

@ -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: <PermissionsSection />,
handle: {
access: (accessChecker) =>
accessChecker.hasAny("view-realm", "view-clients", "view-users"),
},
};
export const toPermissionsTabs = (
params: PermissionsTabsParams,
): Partial<Path> => ({
pathname: generateEncodedPath(PermissionsTabsRoute.path, params),
});

View File

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

View File

@ -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<boolean>(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;
}

View File

@ -24,6 +24,7 @@ export default interface RealmRepresentation {
actionTokenGeneratedByUserLifespan?: number;
adminEventsDetailsEnabled?: boolean;
adminEventsEnabled?: boolean;
adminPermissionsEnabled?: boolean;
adminTheme?: string;
attributes?: Record<string, any>;
// AuthenticationFlowRepresentation