mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
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:
parent
a26e482887
commit
6587f5f76e
@ -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
|
||||
@ -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")} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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")));
|
||||
},
|
||||
|
||||
@ -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,
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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!}
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user