mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Introduced Permissions tab (#35409)
* Introduced Permissions tab Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>
This commit is contained in:
parent
7c4a5aed77
commit
f21bbb26d5
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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";
|
||||
|
||||
223
js/apps/admin-ui/src/permissions/PermissionsSection.tsx
Normal file
223
js/apps/admin-ui/src/permissions/PermissionsSection.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
7
js/apps/admin-ui/src/permissions/routes.ts
Normal file
7
js/apps/admin-ui/src/permissions/routes.ts
Normal 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;
|
||||
21
js/apps/admin-ui/src/permissions/routes/Permissions.tsx
Normal file
21
js/apps/admin-ui/src/permissions/routes/Permissions.tsx
Normal 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),
|
||||
});
|
||||
28
js/apps/admin-ui/src/permissions/routes/PermissionsTabs.tsx
Normal file
28
js/apps/admin-ui/src/permissions/routes/PermissionsTabs.tsx
Normal 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),
|
||||
});
|
||||
@ -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,
|
||||
|
||||
29
js/apps/admin-ui/src/utils/useIsAdminPermissionsClient.ts
Normal file
29
js/apps/admin-ui/src/utils/useIsAdminPermissionsClient.ts
Normal 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;
|
||||
}
|
||||
@ -24,6 +24,7 @@ export default interface RealmRepresentation {
|
||||
actionTokenGeneratedByUserLifespan?: number;
|
||||
adminEventsDetailsEnabled?: boolean;
|
||||
adminEventsEnabled?: boolean;
|
||||
adminPermissionsEnabled?: boolean;
|
||||
adminTheme?: string;
|
||||
attributes?: Record<string, any>;
|
||||
// AuthenticationFlowRepresentation
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user