Added UI support for Clients and Groups resource types (#37379)

* removed policyId from permission search form

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* Adding support for Clients and Groups resource types

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* Adding support for Clients and Groups resource types

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* Added support for clients resourceType

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* Added support for creating permission based on Clients and Groups resource types

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* updated messages

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* updated messages

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* Fixing the search by resource type and resource

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>

* Fixing changing permissions from specific to all resources

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>

* made groups policy default instead of aggregate and fixed ClientSelect

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* Added error handling for authorization scope field

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* Added error handling for authorization scope field

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* improved policy creation from create permission

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* improved policy creation from create permission

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* improved policy creation from create permission

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* Improved ClientScope

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* updated GroupSelect

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

---------

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>
Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
Co-authored-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Agnieszka Gancarczyk 2025-02-24 11:12:09 +00:00 committed by GitHub
parent a26e482887
commit 6587f5f76e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 301 additions and 194 deletions

View File

@ -3360,13 +3360,12 @@ resourceScope=Resource scope
resourceScopeHelpText=Specifies the scope of the resource. This is used to determine the type of resource that the permission is granted to.
allClients=All clients
specificClients=Specific clients
allUsers=All users
specificUsers=Specific users
allResourceType=All {{resourceType}}
specificResourceType=Specific {{resourceType}}
assignedPolicies=Assigned policies
assignExistingPolicies=Assign existing policies
requiredPolicies=Please add at least one policy.
createNewPolicy=Create new policy
createPermissionPolicy=Create policy
policy=Policy
policyType=Policy type
policyTypeHelpText=Specifies the type of policy. This is used to determine the type of policy that the permission is granted to.
@ -3383,7 +3382,6 @@ authorizationScopeDetailsTitle=Authorization scope details
authorizationScopeDetailsSubtitle=Authorization scope defines the actions that can be performed on a resource.
authorizationScopeDetailsName=Name
authorizationScopeDetailsDescription=Description
authorizationScopeDetailsDescriptionText=Lorem ipsum
allResources=All resources
currentRealm=Current realm
recentlyUsed=Recently used
@ -3391,8 +3389,9 @@ viewAll=View all
currentRealmExplain=This realm is selected
removeInvalidUsers=Remove invalid users during searches
removeInvalidUsersHelp=Remove users from the local database if they are not available from the user storage when executing searches. If this is true, users no longer available from their corresponding user storage will be deleted from the local database whenever trying to look up users. If false, then users previously imported from the user storage will be kept in the local database, as read-only and disabled, even if that user is no longer available from the user storage. For example, user was deleted directly from LDAP or the `Users DN` is invalid. Note that this behavior will only happen when the user is not yet cached.
applyPermissionTo=Enforce access to
applyPermissionToHelpText=Specifies the resource that the permission is applied to.
createPermissionPolicy=Create policy
enforceAccessTo=Enforce access to
enforceAccessToHelpText=Specifies the resource that the permission is applied to.
emptyPermissionPoliciesInstructions=No policies exist in this realm.
noPermissionSearchResultsInstructions=No permissions matched your filters.
deleteAdminPermissionConfirm=If you delete permission {{ permission }}, administrators cannot perform the actions on resources that were defined by the permission.
@ -3414,4 +3413,9 @@ authorizationScope.Groups.manage-members=Manages group members
authorizationScope.Groups.manage-membership=Adds or removes group members
authorizationScope.Groups.view=Views this group
authorizationScope.Groups.view-members=Views group members
authorizationScope.IdentityProviders.token-exchange=Allows clients to exchange tokens for tokens issued by this identity provider
authorizationScope.IdentityProviders.token-exchange=Allows clients to exchange tokens for tokens issued by this identity provider
usersResources=Users
clientsResources=Clients
groupsResources=Groups
resourceTypeHelpText=Specifies which {{ resourceType }} are allowed by this permission.
evaluation=Evaluation

View File

@ -1,5 +1,6 @@
import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
import {
FormErrorText,
HelpItem,
KeycloakSelect,
SelectVariant,
@ -29,7 +30,10 @@ export const ScopePicker = ({
}: ScopePickerProps) => {
const { adminClient } = useAdminClient();
const { t } = useTranslation();
const { control } = useFormContext();
const {
control,
formState: { errors },
} = useFormContext();
const [open, setOpen] = useState(false);
const [scopes, setScopes] = useState<ScopeRepresentation[]>();
const [search, setSearch] = useState("");
@ -77,44 +81,50 @@ export const ScopePicker = ({
name="scopes"
defaultValue={[]}
control={control}
rules={isAdminPermissionsClient ? { required: t("requiredField") } : {}}
render={({ field }) => {
const selectedValues = field.value.map((o: Scope) => o.name);
return (
<KeycloakSelect
toggleId="scopes"
variant={SelectVariant.typeaheadMulti}
chipGroupProps={{
numChips: 3,
expandedText: t("hide"),
collapsedText: t("showRemaining"),
}}
onToggle={(val) => setOpen(val)}
isOpen={open}
selections={selectedValues}
onFilter={(value) => {
setSearch(value);
return renderScopes(allScopes || []);
}}
onSelect={(selectedValue) => {
const option =
typeof selectedValue === "string"
? selectedValue
: (selectedValue as Scope).name;
const changedValue = field.value.find(
(o: Scope) => o.name === option,
)
? field.value.filter((item: Scope) => item.name !== option)
: [...field.value, { name: option }];
field.onChange(changedValue);
}}
onClear={() => {
setSearch("");
field.onChange([]);
}}
typeAheadAriaLabel={t("authorizationScopes")}
>
{renderScopes(allScopes || [])}
</KeycloakSelect>
<>
<KeycloakSelect
toggleId="scopes"
variant={SelectVariant.typeaheadMulti}
chipGroupProps={{
numChips: 3,
expandedText: t("hide"),
collapsedText: t("showRemaining"),
}}
onToggle={(val) => setOpen(val)}
isOpen={open}
selections={selectedValues}
onFilter={(value) => {
setSearch(value);
return renderScopes(allScopes || []);
}}
onSelect={(selectedValue) => {
const option =
typeof selectedValue === "string"
? selectedValue
: (selectedValue as Scope).name;
const changedValue = field.value.find(
(o: Scope) => o.name === option,
)
? field.value.filter((item: Scope) => item.name !== option)
: [...field.value, { name: option }];
field.onChange(changedValue);
}}
onClear={() => {
setSearch("");
field.onChange([]);
}}
typeAheadAriaLabel={t("authorizationScopes")}
>
{renderScopes(allScopes || [])}
</KeycloakSelect>
{isAdminPermissionsClient && errors.scopes && (
<FormErrorText message={t("required")} />
)}
</>
);
}}
/>

View File

@ -23,11 +23,9 @@ export type SearchForm = {
uri?: string;
owner?: string;
resourceType?: string;
policyId?: string;
};
type SearchDropdownProps = {
policies?: PolicyRepresentation[];
resources?: UserRepresentation[];
types?: PolicyRepresentation[];
search: SearchForm;
@ -36,7 +34,6 @@ type SearchDropdownProps = {
};
export const SearchDropdown = ({
policies,
resources,
types,
search,
@ -57,9 +54,6 @@ export const SearchDropdown = ({
const [open, toggle] = useToggle();
const [resourceScopes, setResourceScopes] = useState<string[]>([]);
const [localPolicies, setLocalPolicies] = useState<PolicyRepresentation[]>(
policies || [],
);
const selectedType = useWatch({ control: form.control, name: "type" });
const [key, setKey] = useState(0);
@ -71,11 +65,7 @@ export const SearchDropdown = ({
useEffect(() => {
const type = types?.find((item) => item.type === selectedType);
setResourceScopes(type?.scopes || []);
if (policies?.length) {
setLocalPolicies(policies);
}
}, [selectedType, types, policies]);
}, [selectedType, types]);
useEffect(() => {
reset(search);
@ -125,7 +115,7 @@ export const SearchDropdown = ({
)}
{type !== "resource" && (
<SelectControl
name={type !== "adminPermission" ? "type" : "type"}
name={type !== "adminPermission" ? "type" : "resourceType"}
label={type !== "adminPermission" ? t("type") : t("resourceType")}
controller={{
defaultValue: "",
@ -173,21 +163,6 @@ export const SearchDropdown = ({
]}
/>
)}
{type === "adminPermission" && (
<SelectControl
name={"policyId"}
label={t("policy")}
controller={{ defaultValue: search.policyId || "" }}
options={
localPolicies
? localPolicies.map(({ id, name }) => ({
key: id!,
value: name!,
}))
: []
}
/>
)}
<ActionGroup>
<Button
variant="primary"

View File

@ -42,11 +42,10 @@ export const ClientScope = () => {
useFetch(
() => adminClient.clientScopes.find(),
(scopes) => {
(scopes = []) => {
const clientScopes = getValues("clientScopes") || [];
setSelectedScopes(
getValues("clientScopes").map(
(s) => scopes.find((c) => c.id === s.id)!,
),
clientScopes.map((s) => scopes.find((c) => c.id === s.id)!),
);
setScopes(localeSort(scopes, mapByKey("name")));
},

View File

@ -62,7 +62,11 @@ export const ClientSelect = ({
onFilter={(value) => setSearch(value)}
variant={variant}
isDisabled={isDisabled}
options={clients.map(({ clientId }) => clientId!)}
options={clients.map(({ id, clientId }) => ({
key: id!,
value: clientId!,
label: clientId,
}))}
/>
);
};

View File

@ -9,7 +9,6 @@ import {
Alert,
} from "@patternfly/react-core";
import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table";
import { isValidComponentType } from "./permission-configuration/PermissionConfigurationDetails";
type NewPermissionConfigurationDialogProps = {
resourceTypes?: ResourceTypesRepresentation[];
@ -63,8 +62,7 @@ export const NewPermissionConfigurationDialog = ({
>
<Td>{resourceType.type}</Td>
<Td style={{ textWrap: "wrap" }}>
{isValidComponentType(resourceType.type!) &&
t(`resourceType.${resourceType.type}`)}
{t(`resourceType.${resourceType.type}`)}
</Td>
</Tr>
);

View File

@ -206,9 +206,9 @@ export default function PermissionsConfigurationSection() {
</Tab>
{hasViewUsers && (
<Tab
id="evaluate"
data-testid="permissionsEvaluate"
title={<TabTitleText>{t("evaluate")}</TabTitleText>}
id="evaluation"
data-testid="permissionsEvaluation"
title={<TabTitleText>{t("evaluation")}</TabTitleText>}
{...permissionsEvaluateTab}
>
<AuthorizationEvaluate

View File

@ -149,12 +149,6 @@ export const PermissionsConfigurationTab = ({
[key, search, first, max],
);
const policies = useMemo(() => {
return permissions
?.flatMap((permission) => permission?.policies!)
.filter((policy) => policy?.name);
}, [permissions]);
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "deletePermission",
messageKey: t("deleteAdminPermissionConfirm", {
@ -217,7 +211,6 @@ export const PermissionsConfigurationTab = ({
<>
<ToolbarItem>
<SearchDropdown
policies={policies!}
resources={users!}
types={resourceTypes}
search={search}
@ -332,7 +325,7 @@ export const PermissionsConfigurationTab = ({
(resource: ResourceRepresentation, index) => (
<Td key={index}>
<span style={{ marginLeft: "8px" }}>
{resource.displayName}
{resource.displayName || resource.name}
</span>
</Td>
),
@ -371,7 +364,7 @@ export const PermissionsConfigurationTab = ({
<>
{newDialog && (
<NewPermissionConfigurationDialog
resourceTypes={resourceServer?.authorizationSchema!.resourceTypes}
resourceTypes={resourceTypes}
onSelect={(resourceType) =>
navigate(
toCreatePermissionConfiguration({

View File

@ -38,7 +38,7 @@ import { JavaScript } from "../../clients/authorization/policy/JavaScript";
import { LogicSelector } from "../../clients/authorization/policy/LogicSelector";
import { Aggregate } from "../../clients/authorization/policy/Aggregate";
import { capitalize } from "lodash-es";
import { type JSX } from "react";
import { useEffect, type JSX } from "react";
type Policy = Omit<PolicyRepresentation, "roles"> & {
groups?: GroupValue[];
@ -54,10 +54,10 @@ type ComponentsProps = {
const defaultValues: Policy = {
name: "",
description: "",
type: "aggregate",
type: "group",
policies: [],
decisionStrategy: "UNANIMOUS" as DecisionStrategy,
logic: "POSITIVE" as Logic,
decisionStrategy: DecisionStrategy.UNANIMOUS,
logic: Logic.POSITIVE,
};
const COMPONENTS: {
@ -75,7 +75,7 @@ const COMPONENTS: {
role: Role,
time: Time,
js: JavaScript,
default: Aggregate,
default: Group,
} as const;
export const isValidComponentType = (value: string) => value in COMPONENTS;
@ -104,7 +104,7 @@ export const NewPermissionPolicyDialog = ({
defaultValues,
});
const { addAlert, addError } = useAlerts();
const { handleSubmit } = form;
const { handleSubmit, reset } = form;
const isPermissionClient = realmRepresentation?.adminPermissionsEnabled;
const policyTypeSelector = useWatch({
@ -113,31 +113,47 @@ export const NewPermissionPolicyDialog = ({
});
function getComponentType() {
if (isValidComponentType(policyTypeSelector!)) {
return COMPONENTS[policyTypeSelector!];
if (policyTypeSelector && isValidComponentType(policyTypeSelector)) {
return COMPONENTS[policyTypeSelector];
}
return COMPONENTS["default"];
}
const ComponentType = getComponentType();
useEffect(() => {
if (policyTypeSelector) {
const { name, description, decisionStrategy, logic } = form.getValues();
reset({
type: policyTypeSelector,
name,
description,
decisionStrategy,
logic,
});
}
}, [policyTypeSelector, reset, form]);
const save = async (policy: Policy) => {
// Remove entries that only have the boolean set and no id
policy.groups = policy.groups?.filter((g) => g.id);
policy.clientScopes = policy.clientScopes?.filter((c) => c.id);
policy.roles = policy.roles
?.filter((r) => r.id)
.map((r) => ({ ...r, required: r.required || false }));
const { groups, roles, policies, ...rest } = policy;
const cleanedPolicy = {
...rest,
...(groups && groups.length > 0 && { groups }),
...(roles && roles.length > 0 && { roles }),
...(policies && policies.length > 0 && { policies }),
};
try {
const createdPolicy = await adminClient.clients.createPolicy(
{ id: permissionClientId, type: policyTypeSelector! },
policy,
cleanedPolicy,
);
onAssign(createdPolicy);
toggleDialog();
addAlert(t("create" + "PolicySuccess"), AlertVariant.success);
addAlert(t("createPolicySuccess"), AlertVariant.success);
} catch (error) {
addError("policySaveError", error);
}
@ -157,7 +173,10 @@ export const NewPermissionPolicyDialog = ({
>
<Form
id="createPermissionPolicy-form"
onSubmit={handleSubmit(save)}
onSubmit={(e) => {
e.stopPropagation();
handleSubmit(save)(e);
}}
isHorizontal
>
<FormProvider {...form}>

View File

@ -11,7 +11,7 @@ import {
DropdownItem,
PageSection,
} from "@patternfly/react-core";
import { useMemo, useState, type JSX } from "react";
import { useMemo, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
@ -29,18 +29,10 @@ import { toPermissionsConfigurationTabs } from "../routes/PermissionsConfigurati
import PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
import { AssignedPolicies } from "./AssignedPolicies";
import { ScopePicker } from "../../clients/authorization/ScopePicker";
import { Users } from "./permission-type/Users";
import { ResourceType } from "./ResourceType";
import { sortBy } from "lodash-es";
import { NameDescription } from "../../clients/authorization/policy/NameDescription";
const COMPONENTS: {
[index: string]: () => JSX.Element;
} = {
Users: Users,
} as const;
export const isValidComponentType = (value: string) => value in COMPONENTS;
export default function PermissionConfigurationDetails() {
const { adminClient } = useAdminClient();
const { t } = useTranslation();
@ -225,14 +217,6 @@ export default function PermissionConfigurationDetails() {
return <KeycloakSpinner />;
}
function getComponentType() {
return isValidComponentType(resourceType)
? COMPONENTS[resourceType]
: COMPONENTS["js"];
}
const ComponentType = getComponentType();
return (
<>
<DeleteConfirm />
@ -265,7 +249,7 @@ export default function PermissionConfigurationDetails() {
clientId={permissionClientId}
resourceTypeScopes={resourceTypeScopes ?? []}
/>
<ComponentType />
<ResourceType resourceType={resourceType} />
<AssignedPolicies
permissionClientId={permissionClientId}
providers={providers!}

View File

@ -0,0 +1,93 @@
import { useTranslation } from "react-i18next";
import { FormGroup, Radio } from "@patternfly/react-core";
import { HelpItem } from "@keycloak/keycloak-ui-shared";
import { useFormContext } from "react-hook-form";
import { useState, type JSX } from "react";
import { UserSelect } from "../../components/users/UserSelect";
import { ClientSelect } from "../../components/client/ClientSelect";
import { GroupSelect } from "../resource-types/GroupSelect";
type ResourceTypeProps = {
resourceType: string;
};
const COMPONENTS: {
[index: string]: (props: any) => JSX.Element;
} = {
users: UserSelect,
clients: ClientSelect,
groups: GroupSelect,
} as const;
export const isValidComponentType = (value: string) => value in COMPONENTS;
export const ResourceType = ({ resourceType }: ResourceTypeProps) => {
const { t } = useTranslation();
const form = useFormContext();
const resourceIds: string[] = form.getValues("resources");
const [isSpecificResources, setIsSpecificResources] = useState(
resourceIds.some((id) => id !== resourceType),
);
function getComponentType() {
const selectedResourceType = resourceType.toLowerCase();
if (isValidComponentType(selectedResourceType)) {
return COMPONENTS[selectedResourceType];
}
return null;
}
const ComponentType = getComponentType();
return (
<>
<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"
/>
<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"
label={`${resourceType.toLowerCase()}Resources`}
helpText={t("resourceTypeHelpText", { resourceType })}
defaultValue={[]}
variant="typeaheadMulti"
/>
)}
</>
);
};

View File

@ -1,67 +0,0 @@
import { useTranslation } from "react-i18next";
import { FormGroup, Radio } from "@patternfly/react-core";
import { HelpItem } from "@keycloak/keycloak-ui-shared";
import { useFormContext } from "react-hook-form";
import { useState } from "react";
import { UserSelect } from "../../../components/users/UserSelect";
export const Users = () => {
const { t } = useTranslation();
const form = useFormContext();
const resourceIds: string[] = form.getValues("resources");
const [isSpecificUsers, setIsSpecificUsers] = useState(
resourceIds.filter((id) => {
return "Users" !== id;
}).length > 0,
);
return (
<>
<FormGroup
label={t("applyPermissionTo")}
labelIcon={
<HelpItem
helpText={t("applyPermissionToHelpText")}
fieldLabelId="apply-permission-to"
/>
}
fieldId="applyPermissionTo"
hasNoPaddingTop
isRequired
>
<Radio
id="allUsers"
data-testid="allUsers"
isChecked={!isSpecificUsers}
name="applyPermissionTo"
label={t("allUsers")}
onChange={() => {
setIsSpecificUsers(false);
form.setValue("resources", []);
}}
className="pf-v5-u-mb-md"
/>
<Radio
id="specificUsers"
data-testid="specificUsers"
isChecked={isSpecificUsers}
name="applyPermissionTo"
label={t("specificUsers")}
onChange={() => {
setIsSpecificUsers(true);
form.setValue("resources", []);
}}
className="pf-v5-u-mb-md"
/>
</FormGroup>
{isSpecificUsers && (
<UserSelect
name="resources"
helpText={t("permissionUsersHelpText")}
defaultValue={[]}
variant="typeaheadMulti"
/>
)}
</>
);
};

View File

@ -0,0 +1,69 @@
import GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
import type { GroupQuery } from "@keycloak/keycloak-admin-client/lib/resources/groups";
import {
SelectControl,
SelectVariant,
useFetch,
} from "@keycloak/keycloak-ui-shared";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAdminClient } from "../../admin-client";
import type { ComponentProps } from "../../components/dynamic/components";
type GroupSelectProps = Omit<ComponentProps, "convertToName"> & {
variant?: `${SelectVariant}`;
};
export const GroupSelect = ({
name,
label,
helpText,
defaultValue,
isDisabled = false,
required = false,
variant = "typeahead",
}: GroupSelectProps) => {
const { adminClient } = useAdminClient();
const { t } = useTranslation();
const [groups, setGroups] = useState<GroupRepresentation[]>([]);
const [search, setSearch] = useState("");
useFetch(
() => {
const params: GroupQuery = {
max: 20,
};
if (search) {
params.search = search;
}
return adminClient.groups.find(params);
},
(groups) => setGroups(groups),
[search],
);
return (
<SelectControl
name={name!}
label={t(label!)}
labelIcon={t(helpText!)}
controller={{
defaultValue: defaultValue || "",
rules: {
required: {
value: required,
message: t("required"),
},
},
}}
onFilter={(value) => setSearch(value)}
variant={variant}
isDisabled={isDisabled}
options={groups.map(({ id, name }) => ({
key: id!,
value: name!,
label: name,
}))}
/>
);
};

View File

@ -301,6 +301,8 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
// if there is single resource remaining delete it
if (policies.size() == 1 && policy.equals(policies.get(0))) {
authorization.getStoreFactory().getResourceStore().delete(resource.getId());
} else {
policy.removeResource(resource);
}
}
} else {

View File

@ -69,6 +69,7 @@ import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.util.JsonSerialization;
import org.keycloak.utils.StringUtil;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -204,6 +205,7 @@ public class PolicyService {
public Response findAll(@QueryParam("policyId") String id,
@QueryParam("name") String name,
@QueryParam("type") String type,
@QueryParam("resourceType") String resourceType,
@QueryParam("resource") String resource,
@QueryParam("scope") String scope,
@QueryParam("permission") Boolean permission,
@ -285,6 +287,10 @@ public class PolicyService {
search.put(Policy.FilterOption.PERMISSION, new String[] {permission.toString()});
}
if (StringUtil.isNotBlank(resourceType)) {
search.put(Policy.FilterOption.CONFIG, new String[] {"defaultResourceType", resourceType});
}
return Response.ok(
doSearch(firstResult, maxResult, fields, search))
.build();

View File

@ -124,7 +124,7 @@ public class UserManagedPermissionService {
@QueryParam("scope") String scope,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult) {
return delegate.findAll(null, name, "uma", resource, scope, true, identity.getId(), null, firstResult, maxResult);
return delegate.findAll(null, name, "uma", null, resource, scope, true, identity.getId(), null, firstResult, maxResult);
}
private Policy getPolicy(@PathParam("policyId") String policyId) {

View File

@ -207,6 +207,24 @@ public class UserResourceTypePermissionTest extends AbstractPermissionTest {
permissionResources = permission.resources();
assertThat(permissionResources.size(), is(1));
assertThat(permissionResources.get(0).getName(), is(AdminPermissionsSchema.USERS.getType()));
representation.setResources(Set.of(userAlice.getId()));
permission.update(representation);
resources = authorization.resources().resources();
assertThat(resources.size(), is(AdminPermissionsSchema.SCHEMA.getResourceTypes().size() + 1));
permissionResources = permission.resources();
assertThat(permissionResources.size(), is(1));
assertThat(permissionResources.get(0).getName(), is(userAlice.getId()));
createUserPermission(userAlice, userBob);
representation.setResources(Set.of());
permission.update(representation);
resources = authorization.resources().resources();
assertThat(resources.size(), is(AdminPermissionsSchema.SCHEMA.getResourceTypes().size() + 2));
permissionResources = permission.resources();
assertThat(permissionResources.size(), is(1));
assertThat(permissionResources.get(0).getName(), is(AdminPermissionsSchema.USERS.getType()));
}
private ScopePermissionRepresentation createUserPermission(ManagedUser... users) {