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:
Erik Jan de Wit 2025-03-26 14:32:13 +01:00 committed by GitHub
parent f73a3fff79
commit ddc3e6e77e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 863 additions and 329 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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");
});
});
});

View 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();
}

View 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);
});
});

View 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ({

View File

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

View File

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

View File

@ -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 = (

View File

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