mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
initial version of the test and some refactor (#38388)
* initial version of the test and some refactor Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * added policy test Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * remove query all users Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * resourceType instead of type Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * small fix Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * set selected value Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * small ui issues Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * fixed test Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * made tests more atomic Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * change to use v2 Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * skip for now Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * org test fix Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * remove old permissions test Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> --------- Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
parent
f73a3fff79
commit
ddc3e6e77e
2
.github/workflows/js-ci.yml
vendored
2
.github/workflows/js-ci.yml
vendored
@ -241,7 +241,7 @@ jobs:
|
||||
- name: Start Keycloak server
|
||||
run: |
|
||||
tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz
|
||||
keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz:v1,transient-users &> ~/server.log &
|
||||
keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz:v2,transient-users &> ~/server.log &
|
||||
env:
|
||||
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
|
||||
import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
||||
import type PolicyProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyProviderRepresentation";
|
||||
import { SelectControl, TextControl } from "@keycloak/keycloak-ui-shared";
|
||||
import {
|
||||
ActionGroup,
|
||||
@ -8,8 +7,8 @@ import {
|
||||
Form,
|
||||
MenuToggle,
|
||||
} from "@patternfly/react-core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormProvider, useForm, useWatch } from "react-hook-form";
|
||||
import { useEffect } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useToggle from "../../utils/useToggle";
|
||||
|
||||
@ -22,30 +21,23 @@ export type SearchForm = {
|
||||
type?: string;
|
||||
uri?: string;
|
||||
owner?: string;
|
||||
resourceType?: string;
|
||||
};
|
||||
|
||||
type SearchDropdownProps = {
|
||||
resources?: UserRepresentation[];
|
||||
types?: PolicyRepresentation[];
|
||||
types?: PolicyProviderRepresentation[] | PolicyProviderRepresentation[];
|
||||
search: SearchForm;
|
||||
onSearch: (form: SearchForm) => void;
|
||||
type: "resource" | "policy" | "permission" | "adminPermission";
|
||||
type: "resource" | "policy" | "permission";
|
||||
};
|
||||
|
||||
export const SearchDropdown = ({
|
||||
resources,
|
||||
types,
|
||||
search,
|
||||
onSearch,
|
||||
type,
|
||||
}: SearchDropdownProps) => {
|
||||
const { t } = useTranslation();
|
||||
const form = useForm<SearchForm>({
|
||||
mode: "onChange",
|
||||
defaultValues: search,
|
||||
});
|
||||
|
||||
const form = useForm<SearchForm>({ mode: "onChange" });
|
||||
const {
|
||||
reset,
|
||||
formState: { isDirty },
|
||||
@ -53,24 +45,13 @@ export const SearchDropdown = ({
|
||||
} = form;
|
||||
|
||||
const [open, toggle] = useToggle();
|
||||
const [resourceScopes, setResourceScopes] = useState<string[]>([]);
|
||||
const selectedType = useWatch({ control: form.control, name: "type" });
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
const submit = (form: SearchForm) => {
|
||||
toggle();
|
||||
onSearch(form);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const type = types?.find((item) => item.type === selectedType);
|
||||
setResourceScopes(type?.scopes || []);
|
||||
}, [selectedType, types]);
|
||||
|
||||
useEffect(() => {
|
||||
reset(search);
|
||||
setKey((prevKey) => prevKey + 1);
|
||||
}, [search]);
|
||||
useEffect(() => reset(search), [search]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
@ -84,15 +65,13 @@ export const SearchDropdown = ({
|
||||
>
|
||||
{type === "resource" && t("searchClientAuthorizationResource")}
|
||||
{type === "policy" && t("searchClientAuthorizationPolicy")}
|
||||
{(type === "permission" || type === "adminPermission") &&
|
||||
t("searchClientAuthorizationPermission")}
|
||||
{type === "permission" && t("searchClientAuthorizationPermission")}
|
||||
</MenuToggle>
|
||||
)}
|
||||
isOpen={open}
|
||||
>
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
key={key}
|
||||
isHorizontal
|
||||
className="keycloak__client_authentication__searchdropdown_form"
|
||||
onSubmit={handleSubmit(submit)}
|
||||
@ -105,60 +84,22 @@ export const SearchDropdown = ({
|
||||
<TextControl name="owner" label={t("owner")} />
|
||||
</>
|
||||
)}
|
||||
{type !== "resource" &&
|
||||
type !== "policy" &&
|
||||
type !== "adminPermission" && (
|
||||
<TextControl name="resource" label={t("resource")} />
|
||||
)}
|
||||
{type !== "policy" && type !== "adminPermission" && (
|
||||
<TextControl name="scope" label={t("scope")} />
|
||||
{type !== "resource" && type !== "policy" && (
|
||||
<TextControl name="resource" label={t("resource")} />
|
||||
)}
|
||||
{type !== "policy" && <TextControl name="scope" label={t("scope")} />}
|
||||
{type !== "resource" && (
|
||||
<SelectControl
|
||||
name={type !== "adminPermission" ? "type" : "resourceType"}
|
||||
label={type !== "adminPermission" ? t("type") : t("resourceType")}
|
||||
name="type"
|
||||
label={t("type")}
|
||||
controller={{
|
||||
defaultValue: "",
|
||||
}}
|
||||
options={[
|
||||
...(type !== "adminPermission"
|
||||
? [{ key: "", value: t("allTypes") }]
|
||||
: []),
|
||||
...(Array.isArray(types)
|
||||
? types.map(({ type, name }) => ({
|
||||
key: type!,
|
||||
value: name! || type!,
|
||||
}))
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{type === "adminPermission" && (
|
||||
<SelectControl
|
||||
name={"resource"}
|
||||
label={t("resource")}
|
||||
controller={{
|
||||
defaultValue: "",
|
||||
}}
|
||||
options={[
|
||||
...(resources || []).map(({ id, username }) => ({
|
||||
key: id!,
|
||||
value: username!,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{type === "adminPermission" && (
|
||||
<SelectControl
|
||||
name={"scope"}
|
||||
label={t("authorizationScope")}
|
||||
controller={{
|
||||
defaultValue: "",
|
||||
}}
|
||||
options={[
|
||||
...(resourceScopes || []).map((resourceScope) => ({
|
||||
key: resourceScope!,
|
||||
value: resourceScope!,
|
||||
{ key: "", value: t("allTypes") },
|
||||
...(types || []).map(({ type, name }) => ({
|
||||
key: type!,
|
||||
value: name!,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
@ -175,10 +116,7 @@ export const SearchDropdown = ({
|
||||
<Button
|
||||
variant="link"
|
||||
data-testid="revert-btn"
|
||||
onClick={() => {
|
||||
reset({});
|
||||
onSearch({});
|
||||
}}
|
||||
onClick={() => onSearch({})}
|
||||
>
|
||||
{t("clear")}
|
||||
</Button>
|
||||
|
||||
@ -25,8 +25,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { useAdminClient } from "../../admin-client";
|
||||
import useToggle from "../../utils/useToggle";
|
||||
import type { ComponentProps } from "../dynamic/components";
|
||||
import { PermissionsConfigurationTabsParams } from "../../permissions-configuration/routes/PermissionsConfigurationTabs";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
type UserSelectVariant = "typeaheadMulti" | "typeahead";
|
||||
|
||||
@ -59,7 +57,6 @@ export const UserSelect = ({
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [search, setSearch] = useState("");
|
||||
const textInputRef = useRef<HTMLInputElement>();
|
||||
const { tab } = useParams<PermissionsConfigurationTabsParams>();
|
||||
|
||||
const debounceFn = useCallback(debounce(setSearch, 500), []);
|
||||
|
||||
@ -75,7 +72,12 @@ export const UserSelect = ({
|
||||
|
||||
return foundUsers.filter((user) => user !== undefined);
|
||||
},
|
||||
setSelectedUsers,
|
||||
(users) => {
|
||||
setSelectedUsers(users);
|
||||
if (variant !== "typeaheadMulti") {
|
||||
setInputValue(users[0]?.username || "");
|
||||
}
|
||||
},
|
||||
[values],
|
||||
);
|
||||
|
||||
@ -114,14 +116,9 @@ export const UserSelect = ({
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={tab !== "evaluation" ? t(label!) : t("user")}
|
||||
label={t(label!)}
|
||||
isRequired={isRequired}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={helpText!}
|
||||
fieldLabelId={tab !== "evaluation" ? t(label!) : t("user")}
|
||||
/>
|
||||
}
|
||||
labelIcon={<HelpItem helpText={helpText!} fieldLabelId={t(label!)} />}
|
||||
fieldId={name!}
|
||||
>
|
||||
<Controller
|
||||
|
||||
@ -1,38 +1,30 @@
|
||||
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 {
|
||||
PermissionsConfigurationTabs,
|
||||
toPermissionsConfigurationTabs,
|
||||
} from "../permissions-configuration/routes/PermissionsConfigurationTabs";
|
||||
import {
|
||||
AlertVariant,
|
||||
PageSection,
|
||||
Tab,
|
||||
TabTitleText,
|
||||
} from "@patternfly/react-core";
|
||||
import { AuthorizationPolicies } from "../clients/authorization/Policies";
|
||||
import { PermissionsEvaluationTab } from "./permission-evaluation/PermissionsEvaluationTab";
|
||||
import { PermissionsConfigurationTab } from "./permission-configuration/PermissionsConfigurationTab";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { useAccess } from "../context/access/Access";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { FormProvider, useForm, useWatch } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAdminClient } from "../admin-client";
|
||||
import { AuthorizationPolicies } from "../clients/authorization/Policies";
|
||||
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 {
|
||||
RoutableTabs,
|
||||
useRoutableTab,
|
||||
} from "../components/routable-tabs/RoutableTabs";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
import { useAccess } from "../context/access/Access";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { toPermissionsConfigurationTabs } from "../permissions-configuration/routes/PermissionsConfigurationTabs";
|
||||
import { convertFormValuesToObject, convertToFormValues } from "../util";
|
||||
import useToggle from "../utils/useToggle";
|
||||
import { PermissionsConfigurationTab } from "./permission-configuration/PermissionsConfigurationTab";
|
||||
import { PermissionsEvaluationTab } from "./permission-evaluation/PermissionsEvaluationTab";
|
||||
|
||||
export default function PermissionsConfigurationSection() {
|
||||
const { adminClient } = useAdminClient();
|
||||
@ -47,15 +39,6 @@ export default function PermissionsConfigurationSection() {
|
||||
const form = useForm<FormFields>();
|
||||
const { realmRepresentation } = useRealm();
|
||||
|
||||
const usePermissionsConfigurationTabs = (tab: PermissionsConfigurationTabs) =>
|
||||
useRoutableTab(
|
||||
toPermissionsConfigurationTabs({
|
||||
realm,
|
||||
permissionClientId: realmRepresentation?.adminPermissionsClient?.id!,
|
||||
tab,
|
||||
}),
|
||||
);
|
||||
|
||||
const clientAuthenticatorType = useWatch({
|
||||
control: form.control,
|
||||
name: "clientAuthenticatorType",
|
||||
@ -64,20 +47,36 @@ export default function PermissionsConfigurationSection() {
|
||||
|
||||
const hasManageAuthorization = hasAccess("manage-authorization");
|
||||
const hasViewUsers = hasAccess("view-users");
|
||||
const permissionsResourcesTab =
|
||||
usePermissionsConfigurationTabs("permissions");
|
||||
const permissionsPoliciesTab = usePermissionsConfigurationTabs("policies");
|
||||
const permissionsEvaluateTab = usePermissionsConfigurationTabs("evaluation");
|
||||
const permissionsResourcesTab = useRoutableTab(
|
||||
toPermissionsConfigurationTabs({
|
||||
realm,
|
||||
permissionClientId: realmRepresentation?.adminPermissionsClient?.id!,
|
||||
tab: "permissions",
|
||||
}),
|
||||
);
|
||||
const permissionsPoliciesTab = useRoutableTab(
|
||||
toPermissionsConfigurationTabs({
|
||||
realm,
|
||||
permissionClientId: realmRepresentation?.adminPermissionsClient?.id!,
|
||||
tab: "policies",
|
||||
}),
|
||||
);
|
||||
const permissionsEvaluateTab = useRoutableTab(
|
||||
toPermissionsConfigurationTabs({
|
||||
realm,
|
||||
permissionClientId: realmRepresentation?.adminPermissionsClient?.id!,
|
||||
tab: "evaluation",
|
||||
}),
|
||||
);
|
||||
|
||||
useFetch(
|
||||
async () => {
|
||||
const clients = await adminClient.clients.find();
|
||||
return clients;
|
||||
const clients = await adminClient.clients.find({
|
||||
clientId: "admin-permissions",
|
||||
});
|
||||
return clients[0];
|
||||
},
|
||||
(clients) => {
|
||||
const adminPermissionsClient = clients.find(
|
||||
(client) => client.clientId === "admin-permissions",
|
||||
);
|
||||
(adminPermissionsClient) => {
|
||||
setAdminPermissionsClient(adminPermissionsClient!);
|
||||
},
|
||||
[],
|
||||
@ -86,15 +85,6 @@ export default function PermissionsConfigurationSection() {
|
||||
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 (
|
||||
@ -122,16 +112,6 @@ export default function PermissionsConfigurationSection() {
|
||||
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,
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
|
||||
import ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
|
||||
import ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
|
||||
import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
||||
import {
|
||||
KeycloakSpinner,
|
||||
ListEmptyState,
|
||||
PaginatingTableToolbar,
|
||||
useAlerts,
|
||||
@ -29,17 +29,13 @@ import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useAdminClient } from "../../admin-client";
|
||||
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
||||
import { KeycloakSpinner } from "@keycloak/keycloak-ui-shared";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import useToggle from "../../utils/useToggle";
|
||||
import {
|
||||
SearchDropdown,
|
||||
SearchForm,
|
||||
} from "../../clients/authorization/SearchDropdown";
|
||||
import { toCreatePermissionConfiguration } from "../routes/NewPermissionConfiguration";
|
||||
import { AuthorizationScopesDetails } from "../permission-configuration/AuthorizationScopesDetails";
|
||||
import { toPermissionConfigurationDetails } from "../routes/PermissionConfigurationDetails";
|
||||
import useSortedResourceTypes from "../../utils/useSortedResourceTypes";
|
||||
import useToggle from "../../utils/useToggle";
|
||||
import { AuthorizationScopesDetails } from "../permission-configuration/AuthorizationScopesDetails";
|
||||
import { SearchDropdown, SearchForm } from "../resource-types/SearchDropdown";
|
||||
import { toCreatePermissionConfiguration } from "../routes/NewPermissionConfiguration";
|
||||
import { toPermissionConfigurationDetails } from "../routes/PermissionConfigurationDetails";
|
||||
import { NewPermissionConfigurationDialog } from "./NewPermissionConfigurationDialog";
|
||||
|
||||
type PermissionsConfigurationProps = {
|
||||
@ -65,7 +61,6 @@ export const PermissionsConfigurationTab = ({
|
||||
useState<ExpandablePolicyRepresentation[]>();
|
||||
const [selectedPermission, setSelectedPermission] =
|
||||
useState<PolicyRepresentation>();
|
||||
const [users, setUsers] = useState<UserRepresentation[]>();
|
||||
const [search, setSearch] = useState<SearchForm>({});
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(key + 1);
|
||||
@ -76,24 +71,15 @@ export const PermissionsConfigurationTab = ({
|
||||
|
||||
useFetch(
|
||||
async () => {
|
||||
const permissions = adminClient.clients.listPermissionScope({
|
||||
const permissions = await adminClient.clients.listPermissionScope({
|
||||
first,
|
||||
max: max + 1,
|
||||
id: clientId,
|
||||
...search,
|
||||
});
|
||||
|
||||
const users = adminClient.users.find({
|
||||
realm,
|
||||
});
|
||||
|
||||
const [permissionsData, usersData] = await Promise.all([
|
||||
permissions,
|
||||
users,
|
||||
]);
|
||||
|
||||
const processedPermissions = await Promise.all(
|
||||
(permissionsData || []).map(async (permission) => {
|
||||
(permissions || []).map(async (permission) => {
|
||||
const policies = await adminClient.clients.getAssociatedPolicies({
|
||||
id: clientId,
|
||||
permissionId: permission.id!,
|
||||
@ -119,14 +105,10 @@ export const PermissionsConfigurationTab = ({
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
permissionsData: processedPermissions,
|
||||
usersData,
|
||||
};
|
||||
return processedPermissions;
|
||||
},
|
||||
(data) => {
|
||||
setPermissions(data.permissionsData as any[]);
|
||||
setUsers(data.usersData);
|
||||
(permissions) => {
|
||||
setPermissions(permissions as any[]);
|
||||
},
|
||||
[key, search, first, max],
|
||||
);
|
||||
@ -193,11 +175,9 @@ export const PermissionsConfigurationTab = ({
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<SearchDropdown
|
||||
resources={users!}
|
||||
types={resourceTypes}
|
||||
search={search}
|
||||
onSearch={setSearch}
|
||||
type="adminPermission"
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
|
||||
@ -20,21 +20,19 @@ import {
|
||||
SplitItem,
|
||||
Title,
|
||||
} from "@patternfly/react-core";
|
||||
import { BellIcon } from "@patternfly/react-icons";
|
||||
import { useMemo, useState } from "react";
|
||||
import { FormProvider, useForm, useWatch } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAdminClient } from "../../admin-client";
|
||||
import { UserSelect } from "../../components/users/UserSelect";
|
||||
import { ClientSelect } from "../../components/client/ClientSelect";
|
||||
import { GroupSelect } from "../resource-types/GroupSelect";
|
||||
import { FormAccess } from "../../components/form/FormAccess";
|
||||
import { UserSelect } from "../../components/users/UserSelect";
|
||||
import { useAccess } from "../../context/access/Access";
|
||||
import { ForbiddenSection } from "../../ForbiddenSection";
|
||||
import { BellIcon } from "@patternfly/react-icons";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { PermissionEvaluationResult } from "./PermissionEvaluationResult";
|
||||
import { ForbiddenSection } from "../../ForbiddenSection";
|
||||
import useSortedResourceTypes from "../../utils/useSortedResourceTypes";
|
||||
import { RoleSelect } from "../resource-types/RoleSelect";
|
||||
import { PermissionEvaluationResult } from "./PermissionEvaluationResult";
|
||||
import { COMPONENTS } from "../resource-types/ResourceType";
|
||||
|
||||
interface EvaluateFormInputs
|
||||
extends Omit<ResourceEvaluation, "context" | "resources"> {
|
||||
@ -52,13 +50,6 @@ type Props = {
|
||||
save: () => void;
|
||||
} & EvaluationResultRepresentation;
|
||||
|
||||
const COMPONENTS: Record<string, React.ElementType> = {
|
||||
users: UserSelect,
|
||||
clients: ClientSelect,
|
||||
groups: GroupSelect,
|
||||
roles: RoleSelect,
|
||||
};
|
||||
|
||||
export const PermissionsEvaluationTab = (props: Props) => {
|
||||
const { hasAccess } = useAccess();
|
||||
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
import GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
|
||||
import {
|
||||
SelectControl,
|
||||
FormErrorText,
|
||||
HelpItem,
|
||||
SelectVariant,
|
||||
useFetch,
|
||||
} from "@keycloak/keycloak-ui-shared";
|
||||
import { Button, FormGroup } from "@patternfly/react-core";
|
||||
import { MinusCircleIcon } from "@patternfly/react-icons";
|
||||
import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table";
|
||||
import { useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAdminClient } from "../../admin-client";
|
||||
import type { ComponentProps } from "../../components/dynamic/components";
|
||||
import { PermissionsConfigurationTabsParams } from "../routes/PermissionsConfigurationTabs";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { GroupPickerDialog } from "../../components/group/GroupPickerDialog";
|
||||
|
||||
type GroupSelectProps = Omit<ComponentProps, "convertToName"> & {
|
||||
variant?: `${SelectVariant}`;
|
||||
@ -23,42 +27,122 @@ export const GroupSelect = ({
|
||||
defaultValue,
|
||||
isDisabled = false,
|
||||
isRequired,
|
||||
variant = "typeahead",
|
||||
}: GroupSelectProps) => {
|
||||
const { adminClient } = useAdminClient();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
control,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
const values: string[] = getValues(name!);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [groups, setGroups] = useState<GroupRepresentation[]>([]);
|
||||
const { tab } = useParams<PermissionsConfigurationTabsParams>();
|
||||
|
||||
useFetch(
|
||||
() => {
|
||||
return adminClient.groups.find();
|
||||
if (values && values.length > 0) {
|
||||
return Promise.all(
|
||||
(values as string[]).map((id) => adminClient.groups.findOne({ id })),
|
||||
);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
(groups) => {
|
||||
setGroups(groups.flat().filter((g) => g) as GroupRepresentation[]);
|
||||
},
|
||||
(groups) => setGroups(groups),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<SelectControl
|
||||
name={name!}
|
||||
label={tab !== "evaluation" ? t(label!) : t("group")}
|
||||
labelIcon={tab !== "evaluation" ? t(helpText!) : t("selectGroup")}
|
||||
controller={{
|
||||
defaultValue: defaultValue || "",
|
||||
rules: {
|
||||
required: {
|
||||
value: isRequired || false,
|
||||
message: t("required"),
|
||||
},
|
||||
},
|
||||
}}
|
||||
variant={variant}
|
||||
isDisabled={isDisabled}
|
||||
options={groups.map(({ id, name }) => ({
|
||||
key: id!,
|
||||
value: name!,
|
||||
label: name,
|
||||
}))}
|
||||
/>
|
||||
<FormGroup
|
||||
label={t(label!)}
|
||||
labelIcon={<HelpItem helpText={t(helpText!)} fieldLabelId="groups" />}
|
||||
fieldId="groups"
|
||||
isRequired={isRequired}
|
||||
>
|
||||
<Controller
|
||||
name={name!}
|
||||
control={control}
|
||||
defaultValue={defaultValue}
|
||||
rules={
|
||||
isRequired
|
||||
? {
|
||||
validate: (value?: GroupRepresentation[]) =>
|
||||
value && value.length > 0,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
{open && (
|
||||
<GroupPickerDialog
|
||||
type="selectMany"
|
||||
text={{
|
||||
title: "addGroupsToGroupPolicy",
|
||||
ok: "add",
|
||||
}}
|
||||
onConfirm={(selectGroup) => {
|
||||
field.onChange([
|
||||
...(field.value || []),
|
||||
...(selectGroup || []).map(({ id }) => id),
|
||||
]);
|
||||
setGroups([...groups, ...(selectGroup || [])]);
|
||||
setOpen(false);
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
filterGroups={groups}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
data-testid="select-group-button"
|
||||
isDisabled={isDisabled}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("addGroups")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{groups.length > 0 && (
|
||||
<Table variant="compact">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t("groups")}</Th>
|
||||
<Th aria-hidden="true" />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{groups.map((group) => (
|
||||
<Tr key={group.id}>
|
||||
<Td>{group.path}</Td>
|
||||
<Td>
|
||||
<Button
|
||||
variant="link"
|
||||
className="keycloak__client-authorization__policy-row-remove"
|
||||
icon={<MinusCircleIcon />}
|
||||
onClick={() => {
|
||||
setValue(name!, [
|
||||
...(groups || []).filter(({ id }) => id !== group.id),
|
||||
]);
|
||||
setGroups([
|
||||
...groups.filter(({ id }) => id !== group.id),
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
)}
|
||||
{errors.groups && <FormErrorText message={t("requiredGroups")} />}
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
@ -9,10 +9,11 @@ import { RoleSelect } from "./RoleSelect";
|
||||
import { ClientSelectComponent } from "./ClientSelectComponent";
|
||||
|
||||
type ResourceTypeProps = {
|
||||
withEnforceAccessTo?: boolean;
|
||||
resourceType: string;
|
||||
};
|
||||
|
||||
const COMPONENTS: {
|
||||
export const COMPONENTS: {
|
||||
[index: string]: (props: any) => JSX.Element;
|
||||
} = {
|
||||
users: UserSelect,
|
||||
@ -23,14 +24,17 @@ const COMPONENTS: {
|
||||
|
||||
export const isValidComponentType = (value: string) => value in COMPONENTS;
|
||||
|
||||
export const ResourceType = ({ resourceType }: ResourceTypeProps) => {
|
||||
export const ResourceType = ({
|
||||
resourceType,
|
||||
withEnforceAccessTo = true,
|
||||
}: ResourceTypeProps) => {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
const resourceIds: string[] = form.getValues("resources");
|
||||
const normalizedResourceType = resourceType.toLowerCase();
|
||||
|
||||
const [isSpecificResources, setIsSpecificResources] = useState(
|
||||
resourceIds.some((id) => id !== resourceType),
|
||||
resourceIds?.some((id) => id !== resourceType) || !withEnforceAccessTo,
|
||||
);
|
||||
|
||||
function getComponentType() {
|
||||
@ -44,46 +48,48 @@ export const ResourceType = ({ resourceType }: ResourceTypeProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormGroup
|
||||
label={t("enforceAccessTo")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("enforceAccessToHelpText")}
|
||||
fieldLabelId="enforce-access-to"
|
||||
{withEnforceAccessTo && (
|
||||
<FormGroup
|
||||
label={t("enforceAccessTo")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("enforceAccessToHelpText")}
|
||||
fieldLabelId="enforce-access-to"
|
||||
/>
|
||||
}
|
||||
fieldId="EnforceAccessTo"
|
||||
hasNoPaddingTop
|
||||
isRequired
|
||||
>
|
||||
<Radio
|
||||
id="allResources"
|
||||
data-testid="allResources"
|
||||
isChecked={!isSpecificResources}
|
||||
name="EnforceAccessTo"
|
||||
label={t(`allResourceType`, { resourceType })}
|
||||
onChange={() => {
|
||||
setIsSpecificResources(false);
|
||||
form.setValue("resources", []);
|
||||
}}
|
||||
className="pf-v5-u-mb-md"
|
||||
/>
|
||||
}
|
||||
fieldId="EnforceAccessTo"
|
||||
hasNoPaddingTop
|
||||
isRequired
|
||||
>
|
||||
<Radio
|
||||
id="allResources"
|
||||
data-testid="allResources"
|
||||
isChecked={!isSpecificResources}
|
||||
name="EnforceAccessTo"
|
||||
label={t(`allResourceType`, { resourceType })}
|
||||
onChange={() => {
|
||||
setIsSpecificResources(false);
|
||||
form.setValue("resources", []);
|
||||
}}
|
||||
className="pf-v5-u-mb-md"
|
||||
/>
|
||||
<Radio
|
||||
id="specificResources"
|
||||
data-testid="specificResources"
|
||||
isChecked={isSpecificResources}
|
||||
name="EnforceAccessTo"
|
||||
label={t(`specificResourceType`, { resourceType })}
|
||||
onChange={() => {
|
||||
setIsSpecificResources(true);
|
||||
form.setValue("resources", []);
|
||||
}}
|
||||
className="pf-v5-u-mb-md"
|
||||
/>
|
||||
</FormGroup>
|
||||
<Radio
|
||||
id="specificResources"
|
||||
data-testid="specificResources"
|
||||
isChecked={isSpecificResources}
|
||||
name="EnforceAccessTo"
|
||||
label={t(`specificResourceType`, { resourceType })}
|
||||
onChange={() => {
|
||||
setIsSpecificResources(true);
|
||||
form.setValue("resources", []);
|
||||
}}
|
||||
className="pf-v5-u-mb-md"
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{isSpecificResources && ComponentType && (
|
||||
<ComponentType
|
||||
name="resources"
|
||||
name={withEnforceAccessTo ? "resources" : "resource"}
|
||||
label={`${normalizedResourceType}Resources`}
|
||||
helpText={t("resourceTypeHelpText", {
|
||||
resourceType: normalizedResourceType,
|
||||
|
||||
@ -0,0 +1,166 @@
|
||||
import PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
|
||||
import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
||||
import { SelectControl, TextControl } from "@keycloak/keycloak-ui-shared";
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
Dropdown,
|
||||
Form,
|
||||
MenuToggle,
|
||||
} from "@patternfly/react-core";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FormProvider, useForm, useWatch } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useToggle from "../../utils/useToggle";
|
||||
import { ResourceType } from "./ResourceType";
|
||||
|
||||
export type SearchForm = {
|
||||
name?: string;
|
||||
resources?: string;
|
||||
scope?: string;
|
||||
type?: string;
|
||||
uri?: string;
|
||||
owner?: string;
|
||||
resourceType?: string;
|
||||
};
|
||||
|
||||
type SearchDropdownProps = {
|
||||
resources?: UserRepresentation[];
|
||||
types: PolicyRepresentation[];
|
||||
search: SearchForm;
|
||||
onSearch: (form: SearchForm) => void;
|
||||
resourceType?: string;
|
||||
};
|
||||
|
||||
export const SearchDropdown = ({
|
||||
types,
|
||||
search,
|
||||
onSearch,
|
||||
}: SearchDropdownProps) => {
|
||||
const { t } = useTranslation();
|
||||
const form = useForm<SearchForm>({
|
||||
mode: "onChange",
|
||||
defaultValues: search,
|
||||
});
|
||||
|
||||
const {
|
||||
reset,
|
||||
formState: { isDirty },
|
||||
handleSubmit,
|
||||
} = form;
|
||||
|
||||
const [open, toggle] = useToggle();
|
||||
const [resourceScopes, setResourceScopes] = useState<string[]>([]);
|
||||
const selectedType = useWatch({
|
||||
control: form.control,
|
||||
name: "resourceType",
|
||||
defaultValue: "",
|
||||
});
|
||||
const [key, setKey] = useState(0);
|
||||
const ref = useRef("clients");
|
||||
|
||||
const submit = (form: SearchForm) => {
|
||||
toggle();
|
||||
onSearch(form);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const type = types?.find((item) => item.type === selectedType);
|
||||
setResourceScopes(type?.scopes || []);
|
||||
}, [selectedType, types]);
|
||||
|
||||
useEffect(() => {
|
||||
reset(search);
|
||||
setKey((prevKey) => prevKey + 1);
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
toggle={(ref) => (
|
||||
<MenuToggle
|
||||
data-testid="searchdropdown_dorpdown"
|
||||
ref={ref}
|
||||
onClick={toggle}
|
||||
className="keycloak__client_authentication__searchdropdown"
|
||||
>
|
||||
{t("searchClientAuthorizationPermission")}
|
||||
</MenuToggle>
|
||||
)}
|
||||
isOpen={open}
|
||||
>
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
key={key}
|
||||
isHorizontal
|
||||
className="keycloak__client_authentication__searchdropdown_form"
|
||||
onSubmit={handleSubmit(submit)}
|
||||
>
|
||||
<TextControl name="name" label={t("name")} />
|
||||
<SelectControl
|
||||
name="resourceType"
|
||||
label={t("type")}
|
||||
controller={{
|
||||
defaultValue: "",
|
||||
}}
|
||||
options={[
|
||||
{ key: "", value: t("choose") },
|
||||
...types.map(({ type, name }) => ({
|
||||
key: type!,
|
||||
value: name! || type!,
|
||||
})),
|
||||
]}
|
||||
onSelect={(value, onChange) => {
|
||||
if (ref.current !== value) {
|
||||
ref.current = value as string;
|
||||
form.setValue("resources", undefined);
|
||||
}
|
||||
onChange(value);
|
||||
}}
|
||||
/>
|
||||
{selectedType !== "" && (
|
||||
<>
|
||||
<ResourceType
|
||||
resourceType={selectedType || "clients"}
|
||||
withEnforceAccessTo={false}
|
||||
/>
|
||||
<SelectControl
|
||||
name={"scope"}
|
||||
label={t("authorizationScope")}
|
||||
controller={{
|
||||
defaultValue: "",
|
||||
}}
|
||||
options={[
|
||||
...(resourceScopes || []).map((resourceScope) => ({
|
||||
key: resourceScope!,
|
||||
value: resourceScope!,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ActionGroup>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="search-btn"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
{t("search")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
data-testid="revert-btn"
|
||||
onClick={() => {
|
||||
reset({});
|
||||
onSearch({});
|
||||
}}
|
||||
>
|
||||
{t("clear")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
@ -50,6 +50,8 @@ test.describe("Organization CRUD", () => {
|
||||
|
||||
test.describe("Existing organization", () => {
|
||||
const orgName = `org-edit-${uuid()}`;
|
||||
const delOrgName = `org-del-${uuid()}`;
|
||||
const delOrgName2 = `org-del-${uuid()}`;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await adminClient.createOrganization({
|
||||
@ -57,6 +59,16 @@ test.describe("Organization CRUD", () => {
|
||||
name: orgName,
|
||||
domains: [{ name: orgName, verified: false }],
|
||||
});
|
||||
await adminClient.createOrganization({
|
||||
realm: realmName,
|
||||
name: delOrgName,
|
||||
domains: [{ name: delOrgName, verified: false }],
|
||||
});
|
||||
await adminClient.createOrganization({
|
||||
realm: realmName,
|
||||
name: delOrgName2,
|
||||
domains: [{ name: delOrgName2, verified: false }],
|
||||
});
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
@ -79,7 +91,7 @@ test.describe("Organization CRUD", () => {
|
||||
});
|
||||
|
||||
test("should delete from list", async ({ page }) => {
|
||||
await clickRowKebabItem(page, orgName, "Delete");
|
||||
await clickRowKebabItem(page, delOrgName, "Delete");
|
||||
await confirmModal(page);
|
||||
await assertNotificationMessage(
|
||||
page,
|
||||
@ -88,7 +100,7 @@ test.describe("Organization CRUD", () => {
|
||||
});
|
||||
|
||||
test("should delete from details page", async ({ page }) => {
|
||||
await clickTableRowItem(page, orgName);
|
||||
await clickTableRowItem(page, delOrgName2);
|
||||
await selectActionToggleItem(page, "Delete");
|
||||
await confirmModal(page);
|
||||
await assertNotificationMessage(
|
||||
|
||||
170
js/apps/admin-ui/test/permissions/main.spec.ts
Normal file
170
js/apps/admin-ui/test/permissions/main.spec.ts
Normal file
@ -0,0 +1,170 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import adminClient from "../utils/AdminClient";
|
||||
import { clickSaveButton, selectItem } from "../utils/form";
|
||||
import { login } from "../utils/login";
|
||||
import { assertNotificationMessage } from "../utils/masthead";
|
||||
import { goToRealm } from "../utils/sidebar";
|
||||
import { assertRowExists } from "../utils/table";
|
||||
import {
|
||||
clickCreateNewPolicy,
|
||||
clickCreatePermission,
|
||||
clickCreatePolicySaveButton,
|
||||
clickSearchButton,
|
||||
fillUserPermissionForm,
|
||||
goToEvaluation,
|
||||
goToPermissions,
|
||||
openSearchPanel,
|
||||
selectUsersResource,
|
||||
} from "./main";
|
||||
import { fillPolicyForm, goToPolicies } from "./policy";
|
||||
|
||||
test.describe("Permissions section tests", () => {
|
||||
const realmName = `permissions-${uuid()}`;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await adminClient.createRealm(realmName, { adminPermissionsEnabled: true });
|
||||
await adminClient.createUser({
|
||||
realm: realmName,
|
||||
username: "test-user",
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
test.afterAll(() => adminClient.deleteRealm(realmName));
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
await goToRealm(page, realmName);
|
||||
await goToPermissions(page);
|
||||
});
|
||||
|
||||
test("should create permission", async ({ page }) => {
|
||||
await clickCreatePermission(page);
|
||||
await selectUsersResource(page);
|
||||
await fillUserPermissionForm(page, {
|
||||
name: "test-permission",
|
||||
description: "test-description",
|
||||
scopes: ["view"],
|
||||
});
|
||||
await clickCreateNewPolicy(page);
|
||||
await fillPolicyForm(
|
||||
page,
|
||||
{
|
||||
name: "test-policy",
|
||||
description: "test-description",
|
||||
type: "User",
|
||||
user: "test-user",
|
||||
},
|
||||
true,
|
||||
);
|
||||
await clickCreatePolicySaveButton(page);
|
||||
await assertNotificationMessage(page, "Successfully created the policy");
|
||||
|
||||
await expect(
|
||||
page.getByRole("gridcell", { name: "test-policy" }),
|
||||
).toBeVisible();
|
||||
|
||||
await clickSaveButton(page);
|
||||
await assertNotificationMessage(
|
||||
page,
|
||||
"Successfully created the permission",
|
||||
);
|
||||
|
||||
await goToPermissions(page);
|
||||
await assertRowExists(page, "test-permission");
|
||||
await goToPolicies(page);
|
||||
await assertRowExists(page, "test-policy");
|
||||
});
|
||||
|
||||
test.describe("evaluate permissions", () => {
|
||||
test.beforeAll(async () => {
|
||||
await adminClient.createUser({
|
||||
realm: realmName,
|
||||
username: "other-user",
|
||||
enabled: true,
|
||||
});
|
||||
await adminClient.createUser({
|
||||
realm: realmName,
|
||||
username: "user1",
|
||||
enabled: true,
|
||||
});
|
||||
const { id } = await adminClient.createUserPolicy({
|
||||
realm: realmName,
|
||||
name: "other-policy",
|
||||
description: "other-description",
|
||||
type: "user",
|
||||
username: "other-user",
|
||||
});
|
||||
await adminClient.createPermission({
|
||||
realm: realmName,
|
||||
name: "client-permission",
|
||||
description: "",
|
||||
policies: [id!],
|
||||
resources: [],
|
||||
resourceType: "Clients",
|
||||
scopes: ["view"],
|
||||
});
|
||||
});
|
||||
|
||||
test.skip("should evaluate permissions success", async ({ page }) => {
|
||||
await goToEvaluation(page);
|
||||
await selectItem(page, page.getByTestId("user"), "other-user");
|
||||
await selectItem(page, "#resourceType", "Clients");
|
||||
await selectItem(page, "#clients", "account");
|
||||
await selectItem(page, "#authScopes", "view");
|
||||
await page.getByTestId("permission-eval").click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Success alert: Clients with" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test.skip("should evaluate permissions denied", async ({ page }) => {
|
||||
await goToEvaluation(page);
|
||||
await selectItem(page, page.getByTestId("user"), "user1");
|
||||
await selectItem(page, "#resourceType", "Clients");
|
||||
await selectItem(page, "#clients", "account");
|
||||
await selectItem(page, "#authScopes", "view");
|
||||
await page.getByTestId("permission-eval").click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Warning alert: Clients with" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("permission search", () => {
|
||||
test.beforeAll(async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await adminClient.createPermission({
|
||||
realm: realmName,
|
||||
name: `permission-${i}`,
|
||||
description: "",
|
||||
policies: [],
|
||||
resources: [],
|
||||
resourceType: i % 2 ? "Clients" : "Users",
|
||||
scopes: ["view"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("should search permission", async ({ page }) => {
|
||||
await openSearchPanel(page);
|
||||
await page.getByTestId("name").fill("permission-1");
|
||||
await clickSearchButton(page);
|
||||
|
||||
await assertRowExists(page, "permission-1");
|
||||
await assertRowExists(page, "permission-2", false);
|
||||
});
|
||||
|
||||
test("should search permission filter clients", async ({ page }) => {
|
||||
await openSearchPanel(page);
|
||||
await selectItem(page, "#resourceType", "Clients");
|
||||
await clickSearchButton(page);
|
||||
|
||||
await assertRowExists(page, "permission-1");
|
||||
await assertRowExists(page, "permission-2", false);
|
||||
await assertRowExists(page, "permission-3");
|
||||
});
|
||||
});
|
||||
});
|
||||
52
js/apps/admin-ui/test/permissions/main.ts
Normal file
52
js/apps/admin-ui/test/permissions/main.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
|
||||
import { Page } from "@playwright/test";
|
||||
import { selectItem } from "../utils/form";
|
||||
|
||||
export async function goToPermissions(page: Page) {
|
||||
await page.getByTestId("nav-item-permissions").click();
|
||||
}
|
||||
|
||||
export async function goToEvaluation(page: Page) {
|
||||
await page.getByTestId("permissionsEvaluation").click();
|
||||
}
|
||||
|
||||
export async function clickCreatePermission(page: Page) {
|
||||
await page.getByTestId("no-permissions-empty-action").click();
|
||||
}
|
||||
|
||||
export async function selectUsersResource(page: Page) {
|
||||
await page.getByRole("gridcell", { name: "Users", exact: true }).click();
|
||||
}
|
||||
|
||||
export async function fillUserPermissionForm(
|
||||
page: Page,
|
||||
data: PolicyRepresentation,
|
||||
) {
|
||||
const entries = Object.entries(data);
|
||||
for (const [key, value] of entries) {
|
||||
if (key === "scopes") {
|
||||
await selectItem(page, "#scopes", value[0]);
|
||||
continue;
|
||||
}
|
||||
await page.getByTestId(key).fill(value);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clickCreateNewPolicy(page: Page) {
|
||||
await page.getByTestId("select-createNewPolicy-button").click();
|
||||
}
|
||||
|
||||
export async function clickCreatePolicySaveButton(page: Page) {
|
||||
await page
|
||||
.getByRole("dialog", { name: "Create policy" })
|
||||
.getByTestId("save")
|
||||
.click();
|
||||
}
|
||||
|
||||
export async function openSearchPanel(page: Page) {
|
||||
await page.getByTestId("searchdropdown_dorpdown").click();
|
||||
}
|
||||
|
||||
export async function clickSearchButton(page: Page) {
|
||||
await page.getByTestId("search-btn").click();
|
||||
}
|
||||
40
js/apps/admin-ui/test/permissions/policy.spec.ts
Normal file
40
js/apps/admin-ui/test/permissions/policy.spec.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import test from "@playwright/test";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import adminClient from "../utils/AdminClient";
|
||||
import { clickSaveButton } from "../utils/form";
|
||||
import { login } from "../utils/login";
|
||||
import { goToRealm } from "../utils/sidebar";
|
||||
import { goToPermissions } from "./main";
|
||||
import {
|
||||
clickCreateNewPolicy,
|
||||
clickPolicyType,
|
||||
fillPolicyForm,
|
||||
goToPolicies,
|
||||
} from "./policy";
|
||||
|
||||
test.describe("Policy section tests", () => {
|
||||
const realmName = `permissions-policy-${uuid()}`;
|
||||
|
||||
test.beforeAll(() =>
|
||||
adminClient.createRealm(realmName, { adminPermissionsEnabled: true }),
|
||||
);
|
||||
test.afterAll(() => adminClient.deleteRealm(realmName));
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
await goToRealm(page, realmName);
|
||||
await goToPermissions(page);
|
||||
await goToPolicies(page);
|
||||
});
|
||||
|
||||
test("create policy", async ({ page }) => {
|
||||
await clickCreateNewPolicy(page);
|
||||
await clickPolicyType(page, "Client");
|
||||
await fillPolicyForm(page, {
|
||||
name: "test-policy",
|
||||
description: "test-description",
|
||||
client: "broker",
|
||||
});
|
||||
await clickSaveButton(page);
|
||||
});
|
||||
});
|
||||
54
js/apps/admin-ui/test/permissions/policy.ts
Normal file
54
js/apps/admin-ui/test/permissions/policy.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { Page } from "@playwright/test";
|
||||
import { selectItem } from "../utils/form";
|
||||
|
||||
export async function clickCreateNewPolicy(page: Page) {
|
||||
await page.getByTestId("no-policies-empty-action").click();
|
||||
}
|
||||
|
||||
export async function goToPolicies(page: Page) {
|
||||
await page.getByTestId("permissionsPolicies").click();
|
||||
}
|
||||
|
||||
export async function clickPolicyType(page: Page, type: string) {
|
||||
await page.getByRole("gridcell", { name: type, exact: true }).click();
|
||||
}
|
||||
|
||||
type PolicyForm = {
|
||||
name: string;
|
||||
description: string;
|
||||
type?: string;
|
||||
user?: string;
|
||||
client?: string;
|
||||
};
|
||||
|
||||
export async function fillPolicyForm(
|
||||
page: Page,
|
||||
data: PolicyForm,
|
||||
dialog: boolean = false,
|
||||
) {
|
||||
const entries = Object.entries(data);
|
||||
for (const [key, value] of entries) {
|
||||
if (key === "type") {
|
||||
await selectItem(page, "#type", value);
|
||||
continue;
|
||||
}
|
||||
if (key === "user") {
|
||||
await selectItem(
|
||||
page,
|
||||
page.getByRole("combobox", { name: "Type to filter" }),
|
||||
value,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (key === "client") {
|
||||
await selectItem(page, "#clients", value);
|
||||
await page.locator("#clients").click();
|
||||
continue;
|
||||
}
|
||||
|
||||
const locator = dialog
|
||||
? page.getByRole("dialog", { name: "Create policy" })
|
||||
: page;
|
||||
await locator.getByTestId(key).fill(value);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import { test } from "@playwright/test";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import adminClient from "../utils/AdminClient";
|
||||
import { clickSaveButton, switchOn } from "../utils/form";
|
||||
import { login } from "../utils/login";
|
||||
import { assertAxeViolations } from "../utils/masthead";
|
||||
import { goToRealm, goToRealmRoles } from "../utils/sidebar";
|
||||
@ -9,7 +8,6 @@ import { clickTableRowItem } from "../utils/table";
|
||||
|
||||
test.describe("Accessibility tests for realm roles", () => {
|
||||
const realmName = "role-a11y-" + uuid();
|
||||
const role = "a11y-role-" + uuid();
|
||||
const defaultRolesMaster = "default-roles-" + realmName;
|
||||
|
||||
test.beforeAll(() => adminClient.createRealm(realmName));
|
||||
@ -39,21 +37,4 @@ test.describe("Accessibility tests for realm roles", () => {
|
||||
await page.click("text=Create role");
|
||||
await assertAxeViolations(page);
|
||||
});
|
||||
|
||||
test("Check a11y violations on role details", async ({ page }) => {
|
||||
await page.click("text=Create role");
|
||||
await page.fill("input[name='name']", role);
|
||||
await clickSaveButton(page);
|
||||
await assertAxeViolations(page);
|
||||
|
||||
await page.click("text=Attributes");
|
||||
await assertAxeViolations(page);
|
||||
|
||||
await page.click("text=Users in role");
|
||||
await assertAxeViolations(page);
|
||||
|
||||
await page.click("text=Permissions");
|
||||
await switchOn(page, "#permissionsEnabled");
|
||||
await assertAxeViolations(page);
|
||||
});
|
||||
});
|
||||
|
||||
@ -19,6 +19,8 @@ import {
|
||||
} from "../utils/table";
|
||||
import {
|
||||
addClientRolesCondition,
|
||||
addClientScopeCondition,
|
||||
addClientUpdaterSourceHost,
|
||||
assertExists,
|
||||
checkNewClientPolicyForm as assertNewClientPolicyForm,
|
||||
assertRoles,
|
||||
@ -29,7 +31,7 @@ import {
|
||||
createNewClientPolicyFromEmptyState,
|
||||
deleteClientPolicyFromDetails,
|
||||
deleteClientPolicyItemFromTable,
|
||||
deleteClientRolesCondition,
|
||||
deleteCondition,
|
||||
fillClientPolicyForm,
|
||||
fillClientRolesCondition,
|
||||
goBackToPolicies,
|
||||
@ -67,7 +69,7 @@ test.describe("Realm settings client policies tab tests", () => {
|
||||
test("Complete new client form and submit", async ({ page }) => {
|
||||
await goToClientPoliciesList(page);
|
||||
|
||||
await createNewClientPolicyFromEmptyState(page, "New", "Test Description");
|
||||
await createNewClientPolicyFromEmptyState(page, "New", "New Description");
|
||||
await clickSaveClientPolicy(page);
|
||||
await assertNotificationMessage(page, "New policy created");
|
||||
});
|
||||
@ -109,10 +111,10 @@ test.describe("Realm settings client policies tab tests", () => {
|
||||
page,
|
||||
}) => {
|
||||
await clickAddCondition(page);
|
||||
await addClientRolesCondition(page, "manage-realm");
|
||||
await addClientScopeCondition(page);
|
||||
await clickSaveConditionButton(page);
|
||||
await assertNotificationMessage(page, "Condition created successfully.");
|
||||
await assertExists(page, "client-roles-condition-link");
|
||||
await assertExists(page, "client-scopes-condition-link");
|
||||
});
|
||||
|
||||
test("Should edit the client-roles condition of a client profile", async ({
|
||||
@ -135,32 +137,25 @@ test.describe("Realm settings client policies tab tests", () => {
|
||||
test("Should cancel deleting condition from a client profile", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Add a new client-roles condition
|
||||
// Add a new condition
|
||||
const condition = "client-updater-source-host";
|
||||
const conditionLink = `${condition}-condition-link`;
|
||||
await clickAddCondition(page);
|
||||
await addClientRolesCondition(page, "client-roles");
|
||||
await addClientUpdaterSourceHost(page);
|
||||
await clickSaveConditionButton(page);
|
||||
|
||||
await deleteClientRolesCondition(page, "client-roles");
|
||||
await deleteCondition(page, condition);
|
||||
await assertModalTitle(page, "Delete condition?");
|
||||
await assertModalMessage(
|
||||
page,
|
||||
"This action will permanently delete client-roles. This cannot be undone.",
|
||||
`This action will permanently delete ${condition}. This cannot be undone.`,
|
||||
);
|
||||
await cancelModal(page);
|
||||
await assertExists(page, "client-roles-condition-link");
|
||||
await assertExists(page, conditionLink);
|
||||
|
||||
await deleteClientRolesCondition(page, "client-roles");
|
||||
await deleteCondition(page, condition);
|
||||
await confirmModal(page);
|
||||
await assertExists(page, "client-roles-condition-link", false);
|
||||
});
|
||||
|
||||
test("Check deleting the client policy", async ({ page }) => {
|
||||
await goBackToPolicies(page);
|
||||
await goToClientPoliciesList(page);
|
||||
await deleteClientPolicyItemFromTable(page, "Test");
|
||||
await confirmModal(page);
|
||||
await assertNotificationMessage(page, "Client policy deleted");
|
||||
await assertEmptyTable(page);
|
||||
await assertExists(page, conditionLink, false);
|
||||
});
|
||||
|
||||
test("Should not create duplicate client profile", async ({ page }) => {
|
||||
@ -174,21 +169,35 @@ test.describe("Realm settings client policies tab tests", () => {
|
||||
"The name must be unique within the realm",
|
||||
);
|
||||
});
|
||||
|
||||
test("Check deleting newly created client policy from create view via dropdown", async ({
|
||||
page,
|
||||
}) => {
|
||||
await goBackToPolicies(page);
|
||||
await goToClientPoliciesList(page);
|
||||
|
||||
await deleteClientPolicyFromDetails(page, "Test");
|
||||
await confirmModal(page);
|
||||
await assertNotificationMessage(page, "Client policy deleted");
|
||||
await assertEmptyTable(page);
|
||||
});
|
||||
});
|
||||
|
||||
test("Check reloading JSON policies", async ({ page }) => {
|
||||
await shouldReloadJSONPolicies(page);
|
||||
});
|
||||
|
||||
test.describe("Delete client policy", () => {
|
||||
const testPolicy = "DeletablePolicy";
|
||||
test.beforeEach(() =>
|
||||
adminClient.createClientPolicy(testPolicy, "Test Description", realmName),
|
||||
);
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await goToClientPoliciesList(page);
|
||||
});
|
||||
|
||||
test("Check deleting the client policy", async ({ page }) => {
|
||||
await deleteClientPolicyItemFromTable(page, testPolicy);
|
||||
await confirmModal(page);
|
||||
await assertNotificationMessage(page, "Client policy deleted");
|
||||
await assertRowExists(page, testPolicy, false);
|
||||
});
|
||||
|
||||
test("Check deleting newly created client policy from create view via dropdown", async ({
|
||||
page,
|
||||
}) => {
|
||||
await deleteClientPolicyFromDetails(page, testPolicy);
|
||||
await confirmModal(page);
|
||||
await assertNotificationMessage(page, "Client policy deleted");
|
||||
await assertRowExists(page, testPolicy, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Page, expect } from "@playwright/test";
|
||||
import { clickRowKebabItem } from "../utils/table";
|
||||
import { selectItem } from "../utils/form";
|
||||
|
||||
export async function goToClientPoliciesTab(page: Page) {
|
||||
await page.getByTestId("rs-clientPolicies-tab").click();
|
||||
@ -99,6 +100,18 @@ export async function assertRoles(page: Page, role: string) {
|
||||
await expect(page.getByTestId("config.roles0")).toHaveValue(role);
|
||||
}
|
||||
|
||||
export async function addClientScopeCondition(
|
||||
page: Page,
|
||||
scope: string = "Optional",
|
||||
) {
|
||||
await selectConditionType(page, "client-scopes");
|
||||
await selectItem(page, "#type", scope);
|
||||
}
|
||||
|
||||
export async function addClientUpdaterSourceHost(page: Page) {
|
||||
await selectConditionType(page, "client-updater-source-host");
|
||||
}
|
||||
|
||||
export async function addClientRolesCondition(page: Page, role: string) {
|
||||
await selectConditionType(page, "client-roles");
|
||||
await fillClientRolesCondition(page, role);
|
||||
@ -124,7 +137,7 @@ export async function shouldEditClientScopesCondition(page: Page) {
|
||||
await page.getByTestId("save").click();
|
||||
}
|
||||
|
||||
export async function deleteClientRolesCondition(page: Page, name: string) {
|
||||
export async function deleteCondition(page: Page, name: string) {
|
||||
await page.getByTestId(`delete-${name}-condition`).click();
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
goToAttributeGroupsTab,
|
||||
goToAttributesTab,
|
||||
goToUserProfileTab,
|
||||
switchOffIfOn,
|
||||
} from "./userprofile";
|
||||
|
||||
test.describe("User profile tabs", () => {
|
||||
@ -144,7 +145,7 @@ test.describe("User profile tabs", () => {
|
||||
|
||||
await goToUsers(page);
|
||||
await page.getByTestId("no-users-found-empty-action").click();
|
||||
await expect(page.getByTestId(attrName)).not.toBeVisible();
|
||||
await expect(page.getByTestId(attrName)).toBeHidden();
|
||||
await page.getByTestId("username").fill("testuser7");
|
||||
await page.getByTestId("user-creation-save").click();
|
||||
await assertNotificationMessage(page, "The user has been created");
|
||||
@ -167,7 +168,7 @@ test.describe("User profile tabs", () => {
|
||||
await goToUsers(page);
|
||||
await page.getByTestId("no-users-found-empty-action").click();
|
||||
await page.getByTestId("email").fill("testuser8@gmail.com");
|
||||
await expect(page.getByTestId(attrName)).not.toBeVisible();
|
||||
await expect(page.getByTestId(attrName)).toBeHidden();
|
||||
await page.getByTestId("user-creation-save").click();
|
||||
await assertNotificationMessage(page, "The user has been created");
|
||||
|
||||
@ -190,6 +191,7 @@ test.describe("User profile tabs", () => {
|
||||
|
||||
await goToRealmSettings(page);
|
||||
await goToLoginTab(page);
|
||||
await switchOffIfOn(page, "#kc-email-as-username-switch");
|
||||
|
||||
await goToUsers(page);
|
||||
await page.getByTestId("no-users-found-empty-action").click();
|
||||
|
||||
@ -45,3 +45,9 @@ export async function clickSaveValidator(page: Page) {
|
||||
export async function goToAttributeGroupsTab(page: Page) {
|
||||
await page.getByTestId("attributesGroupTab").click();
|
||||
}
|
||||
|
||||
export async function switchOffIfOn(page: Page, selector: string) {
|
||||
if (await page.isChecked(selector)) {
|
||||
await page.locator(selector).click({ force: true });
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,7 +129,7 @@ test.describe("User Fed Kerberos tests", () => {
|
||||
await clickUserFederationCard(page, firstKerberosName);
|
||||
|
||||
await expect(page.getByText(dailyPolicy)).toBeVisible();
|
||||
await expect(page.getByText(defaultPolicy)).not.toBeVisible();
|
||||
await expect(page.getByText(defaultPolicy)).toBeHidden();
|
||||
});
|
||||
|
||||
test("Should set cache policy to evict_weekly", async ({ page }) => {
|
||||
@ -145,7 +145,7 @@ test.describe("User Fed Kerberos tests", () => {
|
||||
await clickUserFederationCard(page, firstKerberosName);
|
||||
|
||||
await expect(page.getByText(weeklyPolicy)).toBeVisible();
|
||||
await expect(page.getByText(defaultPolicy)).not.toBeVisible();
|
||||
await expect(page.getByText(defaultPolicy)).toBeHidden();
|
||||
});
|
||||
|
||||
test("Should set cache policy to max_lifespan", async ({ page }) => {
|
||||
@ -161,7 +161,7 @@ test.describe("User Fed Kerberos tests", () => {
|
||||
await clickUserFederationCard(page, firstKerberosName);
|
||||
|
||||
await expect(page.getByText(lifespanPolicy)).toBeVisible();
|
||||
await expect(page.getByText(defaultPolicy)).not.toBeVisible();
|
||||
await expect(page.getByText(defaultPolicy)).toBeHidden();
|
||||
});
|
||||
|
||||
test("Should set cache policy to no_cache", async ({ page }) => {
|
||||
@ -174,7 +174,7 @@ test.describe("User Fed Kerberos tests", () => {
|
||||
await clickUserFederationCard(page, firstKerberosName);
|
||||
|
||||
await expect(page.getByText(noCachePolicy)).toBeVisible();
|
||||
await expect(page.getByText(defaultPolicy)).not.toBeVisible();
|
||||
await expect(page.getByText(defaultPolicy)).toBeHidden();
|
||||
});
|
||||
|
||||
test("Should edit existing Kerberos provider and cancel", async ({
|
||||
|
||||
@ -3,6 +3,7 @@ import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/
|
||||
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
|
||||
import ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentRepresentation";
|
||||
import OrganizationRepresentation from "@keycloak/keycloak-admin-client/lib/defs/organizationRepresentation";
|
||||
import PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
|
||||
import ProtocolMapperRepresentation from "@keycloak/keycloak-admin-client/lib/defs/protocolMapperRepresentation";
|
||||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
|
||||
@ -624,6 +625,48 @@ class AdminClient {
|
||||
...federatedIdentity,
|
||||
});
|
||||
}
|
||||
|
||||
async #getPermissionClient(realm: string = this.#client.realmName) {
|
||||
const clients = await this.#client.clients.find({
|
||||
realm,
|
||||
clientId: "admin-permissions",
|
||||
});
|
||||
if (clients.length === 0)
|
||||
throw new Error("Client admin-permissions not found");
|
||||
return clients[0];
|
||||
}
|
||||
|
||||
async createPermission({
|
||||
realm,
|
||||
...permission
|
||||
}: PolicyRepresentation & { realm?: string }) {
|
||||
await this.#login();
|
||||
const client = await this.#getPermissionClient(realm);
|
||||
await this.#client.clients.createPermission(
|
||||
{ id: client.id!, type: "scope", realm },
|
||||
permission,
|
||||
);
|
||||
}
|
||||
|
||||
async createUserPolicy({
|
||||
username,
|
||||
realm,
|
||||
...policy
|
||||
}: PolicyRepresentation & { realm?: string; username: string }) {
|
||||
await this.#login();
|
||||
const user = await this.#client.users.find({ username, realm });
|
||||
if (user.length === 0) {
|
||||
throw new Error(`User ${username} not found`);
|
||||
}
|
||||
const client = await this.#getPermissionClient(realm);
|
||||
return this.#client.clients.createPolicy(
|
||||
{ id: client.id!, type: policy.type!, realm },
|
||||
{
|
||||
users: [user[0].id!],
|
||||
...policy,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const adminClient = new AdminClient();
|
||||
|
||||
@ -60,7 +60,7 @@ async function startServer() {
|
||||
path.join(SERVER_DIR, `bin/kc${SCRIPT_EXTENSION}`),
|
||||
[
|
||||
"start-dev",
|
||||
`--features="login:v2,account:v3,admin-fine-grained-authz,transient-users,oid4vc-vci,organization"`,
|
||||
`--features="login:v2,account:v3,admin-fine-grained-authz:v2,transient-users,oid4vc-vci,organization,declarative-ui"`,
|
||||
...keycloakArgs,
|
||||
],
|
||||
{
|
||||
|
||||
@ -49,6 +49,10 @@ export type SelectControlProps<
|
||||
menuAppendTo?: string;
|
||||
placeholderText?: string;
|
||||
chipGroupProps?: ChipGroupProps;
|
||||
onSelect?: (
|
||||
value: string | string[],
|
||||
onChangeHandler: (value: string | string[]) => void,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const isSelectBasedOptions = (
|
||||
|
||||
@ -33,6 +33,7 @@ export const SingleSelectControl = <
|
||||
controller,
|
||||
labelIcon,
|
||||
isDisabled,
|
||||
onSelect,
|
||||
...rest
|
||||
}: SelectControlProps<T, P>) => {
|
||||
const {
|
||||
@ -91,8 +92,13 @@ export const SingleSelectControl = <
|
||||
</MenuToggle>
|
||||
)}
|
||||
onSelect={(_event, v) => {
|
||||
const option = v?.toString();
|
||||
onChange(Array.isArray(value) ? [option] : option);
|
||||
const option = v?.toString()!;
|
||||
const convertedValue = Array.isArray(value) ? [option] : option;
|
||||
if (onSelect) {
|
||||
onSelect(convertedValue, onChange);
|
||||
} else {
|
||||
onChange(convertedValue);
|
||||
}
|
||||
setOpen(false);
|
||||
}}
|
||||
isOpen={open}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user