Refactored a little and fixed bug (#2918)

This commit is contained in:
Erik Jan de Wit 2022-07-06 12:30:40 +02:00 committed by GitHub
parent ab23d21626
commit 6caa64466e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 705 additions and 694 deletions

14
package-lock.json generated
View file

@ -7,7 +7,7 @@
"name": "keycloak-admin-ui",
"license": "Apache",
"dependencies": {
"@keycloak/keycloak-admin-client": "^19.0.0-dev.15",
"@keycloak/keycloak-admin-client": "^19.0.0-dev.16",
"@patternfly/patternfly": "^4.202.1",
"@patternfly/react-code-editor": "^4.65.1",
"@patternfly/react-core": "^4.224.1",
@ -2409,9 +2409,9 @@
}
},
"node_modules/@keycloak/keycloak-admin-client": {
"version": "19.0.0-dev.15",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-19.0.0-dev.15.tgz",
"integrity": "sha512-+NY+afR/Pov9rrOygooFkjrTDzP3auOzQunQQ1mfx2NgG1mk2+uSNehUj0ToWL2AcsdF8s/Z3QlP3Q+BCSUEFg==",
"version": "19.0.0-dev.16",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-19.0.0-dev.16.tgz",
"integrity": "sha512-WSsP+vIvhsf3uQhjYbpVMvuFcCSFwbqOzFyPXtsoCOrW+BPUwLxexkl5pOWGmeqCXDZjxxlCF+8StmCl5zPoMQ==",
"dependencies": {
"axios": "^0.27.2",
"camelize-ts": "^1.0.8",
@ -20126,9 +20126,9 @@
}
},
"@keycloak/keycloak-admin-client": {
"version": "19.0.0-dev.15",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-19.0.0-dev.15.tgz",
"integrity": "sha512-+NY+afR/Pov9rrOygooFkjrTDzP3auOzQunQQ1mfx2NgG1mk2+uSNehUj0ToWL2AcsdF8s/Z3QlP3Q+BCSUEFg==",
"version": "19.0.0-dev.16",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-19.0.0-dev.16.tgz",
"integrity": "sha512-WSsP+vIvhsf3uQhjYbpVMvuFcCSFwbqOzFyPXtsoCOrW+BPUwLxexkl5pOWGmeqCXDZjxxlCF+8StmCl5zPoMQ==",
"requires": {
"axios": "^0.27.2",
"camelize-ts": "^1.0.8",

View file

@ -26,7 +26,7 @@
"server:import-client": "./scripts/import-client.mjs"
},
"dependencies": {
"@keycloak/keycloak-admin-client": "^19.0.0-dev.15",
"@keycloak/keycloak-admin-client": "^19.0.0-dev.16",
"@patternfly/patternfly": "^4.202.1",
"@patternfly/react-code-editor": "^4.65.1",
"@patternfly/react-core": "^4.224.1",

View file

@ -69,6 +69,7 @@
"fineGrainSamlEndpointConfig": "This section to configure exact URLs for Assertion Consumer and Single Logout Service.",
"logoUrl": "URL that references a logo for the Client application",
"policyUrl": "URL that the Relying Party Client provides to the End-User to read about the how the profile data will be used",
"policyUsers": "Specifies which user(s) are allowed by this policy.",
"termsOfServiceUrl": "URL that the Relying Party Client provides to the End-User to read about the Relying Party's terms of service",
"accessTokenSignatureAlgorithm": "JWA algorithm used for signing access tokens.",
"idTokenSignatureAlgorithm": "JWA algorithm used for signing ID tokens.",

View file

@ -12,7 +12,7 @@
"tooltip": "Name of stored user attribute which is the name of an attribute within the UserModel.attribute map."
},
"clientRoleMapping": {
"client": {
"clientId": {
"label": "Client ID",
"tooltip": "Client ID for role mappings. Just client roles of this client will be added to the token. If this is unset, client roles of all clients will be added to the token."
},

View file

@ -9,7 +9,7 @@ import {
TextVariants,
} from "@patternfly/react-core";
import type AccessTokenRepresentation from "@keycloak/keycloak-admin-client/lib/defs/accessTokenAuthorization";
import type AccessTokenRepresentation from "@keycloak/keycloak-admin-client/lib/defs/accessTokenRepresentation";
import { KeycloakTextArea } from "../../components/keycloak-text-area/KeycloakTextArea";
import useToggle from "../../utils/useToggle";
import { prettyPrintJSON } from "../../util";
@ -36,6 +36,7 @@ export const AuthorizationDataModal = ({
<Modal
variant={ModalVariant.medium}
isOpen={show}
aria-label={t("authData")}
header={
<TextContent>
<Text component={TextVariants.h1}>{t("authData")}</Text>

View file

@ -1,6 +1,6 @@
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import React, { useState, KeyboardEvent, useMemo, useRef } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, FormProvider, useForm } from "react-hook-form";
import {
FormGroup,
Select,
@ -11,41 +11,32 @@ import {
Button,
Switch,
ExpandableSection,
TextInput,
ButtonVariant,
InputGroup,
Toolbar,
ToolbarGroup,
ToolbarItem,
Divider,
} from "@patternfly/react-core";
import { Controller, useFormContext } from "react-hook-form";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import type EvaluationResultRepresentation from "@keycloak/keycloak-admin-client/lib/defs/evaluationResultRepresentation";
import type ResourceEvaluation from "@keycloak/keycloak-admin-client/lib/defs/resourceEvaluation";
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
import type PolicyEvaluationResponse from "@keycloak/keycloak-admin-client/lib/defs/policyEvaluationResponse";
import type { KeyValueType } from "../../components/key-value-form/key-value-convert";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { FormPanel } from "../../components/scroll-form/FormPanel";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import type AccessTokenRepresentation from "@keycloak/keycloak-admin-client/lib/defs/accessTokenAuthorization";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import type ResourceEvaluation from "@keycloak/keycloak-admin-client/lib/defs/resourceEvaluation";
import { useRealm } from "../../context/realm-context/RealmContext";
import { KeyBasedAttributeInput } from "./KeyBasedAttributeInput";
import { defaultContextAttributes } from "../utils";
import type EvaluationResultRepresentation from "@keycloak/keycloak-admin-client/lib/defs/evaluationResultRepresentation";
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
import type { KeyValueType } from "../../components/key-value-form/key-value-convert";
import { TableComposable, Th, Thead, Tr } from "@patternfly/react-table";
import { AuthorizationEvaluateResource } from "./AuthorizationEvaluateResource";
import { SearchIcon } from "@patternfly/react-icons";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { useAccess } from "../../context/access/Access";
import { ForbiddenSection } from "../../ForbiddenSection";
import { AuthorizationDataModal } from "./AuthorizationDataModal";
import { Results } from "./evaluate/Results";
import { ClientSelect } from "../../components/client/ClientSelect";
import "./auth-evaluate.css";
import { UserSelect } from "../../components/users/UserSelect";
interface EvaluateFormInputs
extends Omit<ResourceEvaluation, "context" | "resources"> {
@ -54,9 +45,9 @@ interface EvaluateFormInputs
context: {
attributes: Record<string, string>[];
};
resources: Record<string, string>[];
resources?: Record<string, string>[];
client: ClientRepresentation;
user: UserRepresentation;
user: string[];
}
export type AttributeType = {
@ -85,85 +76,40 @@ export type AttributeForm = Omit<
type Props = ClientSettingsProps & EvaluationResultRepresentation;
enum ResultsFilter {
All = "ALL",
StatusDenied = "STATUS_DENIED",
StatusPermitted = "STATUS_PERMITTED",
}
function filterResults(
results: EvaluationResultRepresentation[],
filter: ResultsFilter
) {
switch (filter) {
case ResultsFilter.StatusPermitted:
return results.filter(({ status }) => status === "PERMIT");
case ResultsFilter.StatusDenied:
return results.filter(({ status }) => status === "DENY");
default:
return results;
}
}
export const AuthorizationEvaluate = ({ client }: Props) => {
const form = useFormContext<EvaluateFormInputs>();
const { control, reset, trigger } = form;
const form = useForm<EvaluateFormInputs>({ mode: "onChange" });
const {
control,
register,
reset,
errors,
trigger,
formState: { isValid },
} = form;
const { t } = useTranslation("clients");
const adminClient = useAdminClient();
const realm = useRealm();
const [clientsDropdownOpen, setClientsDropdownOpen] = useState(false);
const [scopesDropdownOpen, setScopesDropdownOpen] = useState(false);
const [userDropdownOpen, setUserDropdownOpen] = useState(false);
const [roleDropdownOpen, setRoleDropdownOpen] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [applyToResourceType, setApplyToResourceType] = useState(false);
const [resources, setResources] = useState<ResourceRepresentation[]>([]);
const [scopes, setScopes] = useState<ScopeRepresentation[]>([]);
const [evaluateResults, setEvaluateResults] = useState<
EvaluationResultRepresentation[]
>([]);
const [access, setAccess] = useState<AccessTokenRepresentation>();
const [showEvaluateResults, setShowEvaluateResults] = useState(false);
const searchQueryRef = useRef("");
const [searchQuery, setSearchQuery] = useState("");
const [filterDropdownOpen, setFilterDropdownOpen] = useState(false);
const [key, setKey] = useState(0);
const [evaluateResult, setEvaluateResult] =
useState<PolicyEvaluationResponse>();
const refresh = () => {
setKey(key + 1);
};
const [filter, setFilter] = useState(ResultsFilter.All);
const [clients, setClients] = useState<ClientRepresentation[]>([]);
const [clientRoles, setClientRoles] = useState<RoleRepresentation[]>([]);
const [users, setUsers] = useState<UserRepresentation[]>([]);
const filteredResources = useMemo(
() =>
filterResults(evaluateResults, filter).filter(
({ resource }) => resource?.name?.includes(searchQuery) ?? false
),
[evaluateResults, filter, searchQuery]
);
const { hasAccess } = useAccess();
if (!hasAccess("view-users"))
return <ForbiddenSection permissionNeeded="view-users" />;
useFetch(
() =>
Promise.all([
adminClient.clients.find(),
adminClient.roles.find(),
adminClient.users.find(),
]),
([clients, roles, users]) => {
setClients(clients);
() => adminClient.roles.find(),
(roles) => {
setClientRoles(roles);
setUsers(users);
},
[]
);
@ -182,7 +128,7 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
setResources(resources);
setScopes(scopes);
},
[key, filter]
[]
);
const evaluate = async () => {
@ -190,13 +136,13 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
return;
}
const formValues = form.getValues();
const keys = formValues.resources.map(({ key }) => key);
const keys = formValues.resources?.map(({ key }) => key);
const resEval: ResourceEvaluation = {
roleIds: formValues.roleIds ?? [],
clientId: formValues.client.id!,
userId: formValues.user.id!,
resources: formValues.resources.filter((resource) =>
keys.includes(resource.name!)
userId: formValues.user[0],
resources: formValues.resources?.filter((resource) =>
keys?.includes(resource.name!)
),
entitlements: false,
context: {
@ -213,442 +159,254 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
resEval
);
setEvaluateResults(evaluation.results);
setAccess(evaluation.rpt);
setShowEvaluateResults(true);
return evaluateResults;
setEvaluateResult(evaluation);
return evaluation;
};
const confirmSearchQuery = () => {
setSearchQuery(searchQueryRef.current);
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
confirmSearchQuery();
}
};
const handleInputChange = (value: string) => {
searchQueryRef.current = value;
};
const noEvaluatedData = evaluateResults.length === 0;
const noFilteredData = filteredResources.length === 0;
return showEvaluateResults ? (
if (evaluateResult) {
return (
<Results
evaluateResult={evaluateResult}
refresh={evaluate}
back={() => setEvaluateResult(undefined)}
/>
);
}
return (
<PageSection>
<Toolbar>
<ToolbarGroup className="providers-toolbar">
<ToolbarItem>
<InputGroup>
<TextInput
name={"inputGroupName"}
id={"inputGroupName"}
type="search"
aria-label={t("common:search")}
placeholder={t("common:search")}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
/>
<Button
variant={ButtonVariant.control}
aria-label={t("common:search")}
onClick={() => confirmSearchQuery()}
>
<SearchIcon />
</Button>
</InputGroup>
</ToolbarItem>
<ToolbarItem>
<Select
width={300}
data-testid="filter-type-select"
isOpen={filterDropdownOpen}
className="kc-filter-type-select"
variant={SelectVariant.single}
onToggle={() => setFilterDropdownOpen(!filterDropdownOpen)}
onSelect={(_, value) => {
setFilter(value as ResultsFilter);
setFilterDropdownOpen(false);
refresh();
}}
selections={filter}
>
<SelectOption
data-testid="all-results-option"
value={ResultsFilter.All}
isPlaceholder
>
{t("allResults")}
</SelectOption>
<SelectOption
data-testid="result-permit-option"
value={ResultsFilter.StatusPermitted}
>
{t("resultPermit")}
</SelectOption>
<SelectOption
data-testid="result-deny-option"
value={ResultsFilter.StatusDenied}
>
{t("resultDeny")}
</SelectOption>
</Select>
</ToolbarItem>
</ToolbarGroup>
</Toolbar>
{!noFilteredData && (
<TableComposable aria-label={t("evaluationResults")}>
<Thead>
<Tr>
<Th />
<Th>{t("resource")}</Th>
<Th>{t("overallResults")}</Th>
<Th>{t("scopes")}</Th>
<Th />
</Tr>
</Thead>
{filteredResources.map((resource, rowIndex) => (
<AuthorizationEvaluateResource
key={rowIndex}
rowIndex={rowIndex}
resource={resource}
evaluateResults={evaluateResults}
/>
))}
</TableComposable>
)}
{(noFilteredData || noEvaluatedData) && (
<>
<Divider />
<ListEmptyState
isSearchVariant
message={t("common:noSearchResults")}
instructions={t("common:noSearchResultsInstructions")}
/>
</>
)}
<ActionGroup className="kc-evaluated-options">
<Button
data-testid="authorization-eval"
id="back-btn"
onClick={() => setShowEvaluateResults(false)}
<FormProvider {...form}>
<FormPanel
className="kc-identity-information"
title={t("clients:identityInformation")}
>
{t("common:back")}
</Button>
<Button
data-testid="authorization-reevaluate"
id="reevaluate-btn"
variant="secondary"
onClick={() => evaluate()}
>
{t("clients:reevaluate")}
</Button>
<AuthorizationDataModal data={access!} />
</ActionGroup>
</PageSection>
) : (
<PageSection>
<FormPanel
className="kc-identity-information"
title={t("clients:identityInformation")}
>
<FormAccess
isHorizontal
role="view-clients"
onSubmit={form.handleSubmit(evaluate)}
>
<FormGroup
label={t("client")}
isRequired
labelIcon={
<HelpItem
helpText="clients-help:client"
fieldLabelId="clients:client"
/>
}
fieldId="client"
>
<Controller
<FormAccess isHorizontal role="view-clients">
<ClientSelect
name="client"
defaultValue={client}
control={control}
render={({ onChange, value }) => (
<Select
toggleId="client"
onToggle={setClientsDropdownOpen}
onSelect={(_, value) => {
onChange(value);
setClientsDropdownOpen(false);
}}
selections={value.clientId}
variant={SelectVariant.typeahead}
aria-label={t("client")}
isOpen={clientsDropdownOpen}
>
{clients.map((client) => (
<SelectOption
selected={client.id === value.id}
key={client.clientId}
value={client}
>
{client.clientId}
</SelectOption>
))}
</Select>
)}
label="client"
namespace="clients"
helpText={"clients-help:client"}
defaultValue={client.clientId}
/>
</FormGroup>
<FormGroup
label={t("user")}
isRequired
labelIcon={
<HelpItem
helpText="clients-help:userSelect"
fieldLabelId="clients:userSelect"
/>
}
fieldId="user"
>
<Controller
<UserSelect
name="user"
rules={{
required: true,
}}
label="users"
helpText="clients-help:selectUser"
defaultValue=""
control={control}
render={({ onChange, value }) => (
<Select
toggleId="user"
placeholderText={t("selectAUser")}
onToggle={setUserDropdownOpen}
onSelect={(_, value) => {
onChange(value);
setUserDropdownOpen(false);
}}
selections={value.username}
variant={SelectVariant.typeahead}
aria-label={t("user")}
isOpen={userDropdownOpen}
>
{users.map((user) => (
<SelectOption
selected={user.username === value.username}
key={user.username}
value={user}
>
{user.username}
</SelectOption>
))}
</Select>
)}
/>
</FormGroup>
<FormGroup
label={t("roles")}
labelIcon={
<HelpItem
helpText="clients-help:roles"
fieldLabelId="clients:roles"
/>
}
fieldId="realmRole"
>
<Controller
name="roleIds"
placeholderText={t("selectARole")}
control={control}
defaultValue={[]}
render={({ onChange, value }) => (
<Select
variant={SelectVariant.typeaheadMulti}
toggleId="role"
onToggle={setRoleDropdownOpen}
selections={value}
onSelect={(_, v) => {
const option = v.toString();
if (value.includes(option)) {
onChange(value.filter((item: string) => item !== option));
} else {
onChange([...value, option]);
}
setRoleDropdownOpen(false);
}}
onClear={(event) => {
event.stopPropagation();
onChange([]);
}}
aria-label={t("realmRole")}
isOpen={roleDropdownOpen}
>
{clientRoles.map((role) => (
<SelectOption
selected={role.name === value}
key={role.name}
value={role.name}
/>
))}
</Select>
)}
/>
</FormGroup>
</FormAccess>
</FormPanel>
<FormPanel className="kc-permissions" title={t("common:permissions")}>
<FormAccess isHorizontal role="view-clients">
<FormGroup
label={t("applyToResourceType")}
fieldId="applyToResourceType"
labelIcon={
<HelpItem
helpText="clients-help:applyToResourceType"
fieldLabelId="clients:applyToResourceType"
/>
}
>
<Switch
id="applyToResource-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={applyToResourceType}
onChange={setApplyToResourceType}
/>
</FormGroup>
{!applyToResourceType ? (
<FormGroup
label={t("resourcesAndAuthScopes")}
id="resourcesAndAuthScopes"
variant={SelectVariant.typeahead}
isRequired
/>
<FormGroup
label={t("roles")}
labelIcon={
<HelpItem
helpText={t("clients-help:contextualAttributes")}
fieldLabelId={`resourcesAndAuthScopes`}
helpText="clients-help:roles"
fieldLabelId="clients:roles"
/>
}
fieldId="realmRole"
validated={errors.roleIds ? "error" : "default"}
helperTextInvalid={t("common:required")}
fieldId="resourcesAndAuthScopes"
isRequired
>
<KeyBasedAttributeInput
selectableValues={resources.map<AttributeType>((item) => ({
name: item.name!,
key: item._id!,
}))}
resources={resources}
name="resources"
<Controller
name="roleIds"
placeholderText={t("selectARole")}
control={control}
defaultValue={[]}
rules={{ validate: (value) => value.length > 0 }}
render={({ onChange, value }) => (
<Select
variant={SelectVariant.typeaheadMulti}
toggleId="role"
onToggle={setRoleDropdownOpen}
selections={value}
onSelect={(_, v) => {
const option = v.toString();
if (value.includes(option)) {
onChange(
value.filter((item: string) => item !== option)
);
} else {
onChange([...value, option]);
}
setRoleDropdownOpen(false);
}}
onClear={(event) => {
event.stopPropagation();
onChange([]);
}}
aria-label={t("realmRole")}
isOpen={roleDropdownOpen}
>
{clientRoles.map((role) => (
<SelectOption
selected={role.name === value}
key={role.name}
value={role.name}
/>
))}
</Select>
)}
/>
</FormGroup>
) : (
<>
</FormAccess>
</FormPanel>
<FormPanel className="kc-permissions" title={t("common:permissions")}>
<FormAccess isHorizontal role="view-clients">
<FormGroup
label={t("applyToResourceType")}
fieldId="applyToResourceType"
labelIcon={
<HelpItem
helpText="clients-help:applyToResourceType"
fieldLabelId="clients:applyToResourceType"
/>
}
>
<Switch
id="applyToResource-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={applyToResourceType}
onChange={setApplyToResourceType}
/>
</FormGroup>
{!applyToResourceType ? (
<FormGroup
label={t("resourceType")}
label={t("resourcesAndAuthScopes")}
id="resourcesAndAuthScopes"
isRequired
labelIcon={
<HelpItem
helpText="clients-help:resourceType"
fieldLabelId="clients:resourceType"
helpText={t("clients-help:contextualAttributes")}
fieldLabelId={`resourcesAndAuthScopes`}
/>
}
fieldId="client"
helperTextInvalid={t("common:required")}
fieldId="resourcesAndAuthScopes"
>
<KeycloakTextInput
type="text"
id="alias"
name="alias"
data-testid="alias"
ref={form.register({ required: true })}
<KeyBasedAttributeInput
selectableValues={resources.map<AttributeType>((item) => ({
name: item.name!,
key: item._id!,
}))}
resources={resources}
name="resources"
/>
</FormGroup>
) : (
<>
<FormGroup
label={t("resourceType")}
isRequired
labelIcon={
<HelpItem
helpText="clients-help:resourceType"
fieldLabelId="clients:resourceType"
/>
}
fieldId="client"
validated={form.errors.alias ? "error" : "default"}
helperTextInvalid={t("common:required")}
>
<KeycloakTextInput
type="text"
id="alias"
name="alias"
data-testid="alias"
ref={register({ required: true })}
/>
</FormGroup>
<FormGroup
label={t("authScopes")}
labelIcon={
<HelpItem
helpText="clients-help:scopesSelect"
fieldLabelId="clients:client"
/>
}
fieldId="authScopes"
>
<Controller
name="authScopes"
defaultValue={[]}
control={control}
render={({ onChange, value }) => (
<Select
toggleId="authScopes"
onToggle={setScopesDropdownOpen}
onSelect={(_, v) => {
const option = v.toString();
if (value.includes(option)) {
onChange(
value.filter((item: string) => item !== option)
);
} else {
onChange([...value, option]);
}
setScopesDropdownOpen(false);
}}
selections={value}
variant={SelectVariant.typeaheadMulti}
aria-label={t("authScopes")}
isOpen={scopesDropdownOpen}
>
{scopes.map((scope) => (
<SelectOption
selected={scope.name === value}
key={scope.id}
value={scope.name}
/>
))}
</Select>
)}
/>
</FormGroup>
</>
)}
<ExpandableSection
toggleText={t("contextualInfo")}
onToggle={() => setIsExpanded(!isExpanded)}
isExpanded={isExpanded}
>
<FormGroup
label={t("authScopes")}
label={t("contextualAttributes")}
id="contextualAttributes"
labelIcon={
<HelpItem
helpText="clients-help:scopesSelect"
fieldLabelId="clients:client"
helpText={t("clients-help:contextualAttributes")}
fieldLabelId={`contextualAttributes`}
/>
}
fieldId="authScopes"
helperTextInvalid={t("common:required")}
fieldId="contextualAttributes"
>
<Controller
name="authScopes"
defaultValue={[]}
control={control}
render={({ onChange, value }) => (
<Select
toggleId="authScopes"
onToggle={setScopesDropdownOpen}
onSelect={(_, v) => {
const option = v.toString();
if (value.includes(option)) {
onChange(
value.filter((item: string) => item !== option)
);
} else {
onChange([...value, option]);
}
setScopesDropdownOpen(false);
}}
selections={value}
variant={SelectVariant.typeaheadMulti}
aria-label={t("authScopes")}
isOpen={scopesDropdownOpen}
>
{scopes.map((scope) => (
<SelectOption
selected={scope.name === value}
key={scope.id}
value={scope.name}
/>
))}
</Select>
)}
<KeyBasedAttributeInput
selectableValues={defaultContextAttributes}
name="context.attributes"
/>
</FormGroup>
</>
)}
<ExpandableSection
toggleText={t("contextualInfo")}
onToggle={() => setIsExpanded(!isExpanded)}
isExpanded={isExpanded}
>
<FormGroup
label={t("contextualAttributes")}
id="contextualAttributes"
labelIcon={
<HelpItem
helpText={t("clients-help:contextualAttributes")}
fieldLabelId={`contextualAttributes`}
/>
}
helperTextInvalid={t("common:required")}
fieldId="contextualAttributes"
</ExpandableSection>
</FormAccess>
<ActionGroup>
<Button
data-testid="authorization-eval"
isDisabled={!isValid}
onClick={() => evaluate()}
>
<KeyBasedAttributeInput
selectableValues={defaultContextAttributes}
name="context.attributes"
/>
</FormGroup>
</ExpandableSection>
</FormAccess>
<ActionGroup>
<Button
data-testid="authorization-eval"
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
isDisabled={form.getValues().resources?.every((e) => e.key === "")}
onClick={() => evaluate()}
>
{t("evaluate")}
</Button>
<Button
data-testid="authorization-revert"
variant="link"
onClick={() => reset()}
>
{t("common:revert")}
</Button>
</ActionGroup>
</FormPanel>
{t("evaluate")}
</Button>
<Button
data-testid="authorization-revert"
variant="link"
onClick={() => reset()}
>
{t("common:revert")}
</Button>
</ActionGroup>
</FormPanel>
</FormProvider>
</PageSection>
);
};

View file

@ -0,0 +1,193 @@
import React, { KeyboardEvent, useMemo, useState } from "react";
import {
Select,
SelectVariant,
SelectOption,
PageSection,
ActionGroup,
Button,
TextInput,
ButtonVariant,
InputGroup,
Toolbar,
ToolbarGroup,
ToolbarItem,
Divider,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { SearchIcon } from "@patternfly/react-icons";
import { TableComposable, Th, Thead, Tr } from "@patternfly/react-table";
import type EvaluationResultRepresentation from "@keycloak/keycloak-admin-client/lib/defs/evaluationResultRepresentation";
import type PolicyEvaluationResponse from "@keycloak/keycloak-admin-client/lib/defs/policyEvaluationResponse";
import { AuthorizationEvaluateResource } from "../AuthorizationEvaluateResource";
import { ListEmptyState } from "../../../components/list-empty-state/ListEmptyState";
import { AuthorizationDataModal } from "../AuthorizationDataModal";
import useToggle from "../../../utils/useToggle";
type ResultProps = {
evaluateResult: PolicyEvaluationResponse;
refresh: () => void;
back: () => void;
};
enum ResultsFilter {
All = "ALL",
StatusDenied = "STATUS_DENIED",
StatusPermitted = "STATUS_PERMITTED",
}
function filterResults(
results: EvaluationResultRepresentation[],
filter: ResultsFilter
) {
switch (filter) {
case ResultsFilter.StatusPermitted:
return results.filter(({ status }) => status === "PERMIT");
case ResultsFilter.StatusDenied:
return results.filter(({ status }) => status === "DENY");
default:
return results;
}
}
export const Results = ({ evaluateResult, refresh, back }: ResultProps) => {
const { t } = useTranslation("clients");
const [filterDropdownOpen, toggleFilterDropdown] = useToggle();
const [filter, setFilter] = useState(ResultsFilter.All);
const [searchQuery, setSearchQuery] = useState("");
const [searchInput, setSearchInput] = useState("");
const confirmSearchQuery = () => {
setSearchQuery(searchInput);
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
confirmSearchQuery();
}
};
const filteredResources = useMemo(
() =>
filterResults(evaluateResult.results!, filter).filter(
({ resource }) => resource?.name?.includes(searchQuery) ?? false
),
[evaluateResult.results, filter, searchQuery]
);
const noEvaluatedData = evaluateResult.results!.length === 0;
const noFilteredData = filteredResources.length === 0;
return (
<PageSection>
<Toolbar>
<ToolbarGroup className="providers-toolbar">
<ToolbarItem>
<InputGroup>
<TextInput
name={"inputGroupName"}
id={"inputGroupName"}
type="search"
aria-label={t("common:search")}
placeholder={t("common:search")}
onChange={setSearchInput}
onKeyDown={handleKeyDown}
/>
<Button
variant={ButtonVariant.control}
aria-label={t("common:search")}
onClick={() => confirmSearchQuery()}
>
<SearchIcon />
</Button>
</InputGroup>
</ToolbarItem>
<ToolbarItem>
<Select
width={300}
data-testid="filter-type-select"
isOpen={filterDropdownOpen}
className="kc-filter-type-select"
variant={SelectVariant.single}
onToggle={toggleFilterDropdown}
onSelect={(_, value) => {
setFilter(value as ResultsFilter);
toggleFilterDropdown();
refresh();
}}
selections={filter}
>
<SelectOption
data-testid="all-results-option"
value={ResultsFilter.All}
isPlaceholder
>
{t("allResults")}
</SelectOption>
<SelectOption
data-testid="result-permit-option"
value={ResultsFilter.StatusPermitted}
>
{t("resultPermit")}
</SelectOption>
<SelectOption
data-testid="result-deny-option"
value={ResultsFilter.StatusDenied}
>
{t("resultDeny")}
</SelectOption>
</Select>
</ToolbarItem>
</ToolbarGroup>
</Toolbar>
{!noFilteredData && (
<TableComposable aria-label={t("evaluationResults")}>
<Thead>
<Tr>
<Th />
<Th>{t("resource")}</Th>
<Th>{t("overallResults")}</Th>
<Th>{t("scopes")}</Th>
<Th />
</Tr>
</Thead>
{filteredResources.map((resource, rowIndex) => (
<AuthorizationEvaluateResource
key={rowIndex}
rowIndex={rowIndex}
resource={resource}
evaluateResults={evaluateResult.results}
/>
))}
</TableComposable>
)}
{(noFilteredData || noEvaluatedData) && (
<>
<Divider />
<ListEmptyState
isSearchVariant
message={t("common:noSearchResults")}
instructions={t("common:noSearchResultsInstructions")}
/>
</>
)}
<ActionGroup className="kc-evaluated-options">
<Button data-testid="authorization-eval" id="back-btn" onClick={back}>
{t("common:back")}
</Button>
<Button
data-testid="authorization-reevaluate"
id="reevaluate-btn"
variant="secondary"
onClick={refresh}
>
{t("clients:reevaluate")}
</Button>
<AuthorizationDataModal data={evaluateResult.rpt!} />
</ActionGroup>
</PageSection>
);
};

View file

@ -1,114 +1,13 @@
import React, { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import {
SelectOption,
FormGroup,
Select,
SelectVariant,
} from "@patternfly/react-core";
import React from "react";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import type { UserQuery } from "@keycloak/keycloak-admin-client/lib/resources/users";
import { HelpItem } from "../../../components/help-enabler/HelpItem";
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
import { useTranslation } from "react-i18next";
import { UserSelect } from "../../../components/users/UserSelect";
export const User = () => {
const { t } = useTranslation("clients");
const {
control,
getValues,
formState: { errors },
} = useFormContext();
const values: string[] | undefined = getValues("users");
const [open, setOpen] = useState(false);
const [users, setUsers] = useState<UserRepresentation[]>([]);
const [search, setSearch] = useState("");
const adminClient = useAdminClient();
useFetch(
async () => {
const params: UserQuery = {
max: 20,
};
if (search) {
params.name = search;
}
if (values?.length && !search) {
return await Promise.all(
values.map(
(id: string) =>
adminClient.users.findOne({ id }) as UserRepresentation
)
);
}
return await adminClient.users.find(params);
},
setUsers,
[search]
);
const convert = (clients: UserRepresentation[]) =>
clients.map((option) => (
<SelectOption
key={option.id!}
value={option.id}
selected={values?.includes(option.id!)}
>
{option.username}
</SelectOption>
));
return (
<FormGroup
label={t("users")}
labelIcon={
<HelpItem
helpText="clients-help:policyUsers"
fieldLabelId="clients:users"
/>
}
fieldId="users"
helperTextInvalid={t("common:required")}
validated={errors.users ? "error" : "default"}
isRequired
>
<Controller
name="users"
defaultValue={[]}
control={control}
rules={{
validate: (value) => value.length > 0,
}}
render={({ onChange, value }) => (
<Select
toggleId="users"
variant={SelectVariant.typeaheadMulti}
onToggle={(open) => setOpen(open)}
isOpen={open}
selections={value}
onFilter={(_, value) => {
setSearch(value);
return convert(users);
}}
onSelect={(_, v) => {
const option = v.toString();
if (value.includes(option)) {
onChange(value.filter((item: string) => item !== option));
} else {
onChange([...value, option]);
}
setOpen(false);
}}
aria-label={t("users")}
>
{convert(users)}
</Select>
)}
/>
</FormGroup>
);
};
export const User = () => (
<UserSelect
name="users"
label="users"
helpText="clients-help:policyUsers"
defaultValue={[]}
isRequired
/>
);

View file

@ -0,0 +1,103 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
FormGroup,
Select,
SelectOption,
SelectVariant,
} from "@patternfly/react-core";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type { ClientQuery } from "@keycloak/keycloak-admin-client/lib/resources/clients";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { HelpItem } from "../help-enabler/HelpItem";
import type { ComponentProps } from "../dynamic/components";
type ClientSelectProps = ComponentProps & {
namespace: string;
required?: boolean;
};
export const ClientSelect = ({
name,
label,
helpText,
defaultValue,
namespace,
isDisabled = false,
required = false,
}: ClientSelectProps) => {
const { t } = useTranslation(namespace);
const { control, errors } = useFormContext();
const [open, setOpen] = useState(false);
const [clients, setClients] = useState<ClientRepresentation[]>([]);
const [search, setSearch] = useState("");
const adminClient = useAdminClient();
useFetch(
() => {
const params: ClientQuery = {
max: 20,
};
if (search) {
params.clientId = search;
params.search = true;
}
return adminClient.clients.find(params);
},
(clients) => setClients(clients),
[search]
);
const convert = (clients: ClientRepresentation[]) =>
clients.map((option) => (
<SelectOption key={option.id} value={option.clientId} />
));
return (
<FormGroup
label={t(label!)}
isRequired={required}
labelIcon={
<HelpItem
helpText={t(helpText!)}
fieldLabelId={`${namespace}:${label}`}
/>
}
fieldId={name!}
validated={errors[name!] ? "error" : "default"}
helperTextInvalid={t("common:required")}
>
<Controller
name={name!}
defaultValue={defaultValue || ""}
control={control}
rules={required ? { required: true } : {}}
render={({ onChange, value }) => (
<Select
toggleId={name}
variant={SelectVariant.typeahead}
onToggle={(open) => setOpen(open)}
isOpen={open}
isDisabled={isDisabled}
selections={value}
onFilter={(_, value) => {
setSearch(value);
return convert(clients);
}}
onSelect={(_, value) => {
onChange(value.toString());
setOpen(false);
}}
aria-label={t(label!)}
>
{convert(clients)}
</Select>
)}
/>
</FormGroup>
);
};

View file

@ -1,88 +1,14 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
FormGroup,
Select,
SelectOption,
SelectVariant,
} from "@patternfly/react-core";
import React from "react";
import type { ClientQuery } from "@keycloak/keycloak-admin-client/lib/resources/clients";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { HelpItem } from "../help-enabler/HelpItem";
import type { ComponentProps } from "./components";
import { ClientSelect } from "../client/ClientSelect";
export const ClientSelectComponent = ({
name,
label,
helpText,
defaultValue,
isDisabled = false,
}: ComponentProps) => {
const { t } = useTranslation("dynamic");
const { control } = useFormContext();
const [open, setOpen] = useState(false);
const [clients, setClients] = useState<JSX.Element[]>();
const [search, setSearch] = useState("");
const adminClient = useAdminClient();
useFetch(
() => {
const params: ClientQuery = {
max: 20,
};
if (search) {
params.clientId = search;
params.search = true;
}
return adminClient.clients.find(params);
},
(clients) =>
setClients(
clients.map((option) => (
<SelectOption key={option.id} value={option.clientId} />
))
),
[search]
);
export const ClientSelectComponent = (props: ComponentProps) => {
return (
<FormGroup
label={t(label!)}
labelIcon={
<HelpItem helpText={t(helpText!)} fieldLabelId={`dynamic:${label}`} />
}
fieldId={name!}
>
<Controller
name={`config.${name}`}
defaultValue={defaultValue || ""}
control={control}
render={({ onChange, value }) => (
<Select
toggleId={name}
variant={SelectVariant.typeahead}
onToggle={(open) => setOpen(open)}
isOpen={open}
isDisabled={isDisabled}
selections={value}
onFilter={(_, value) => {
setSearch(value);
return clients;
}}
onSelect={(_, value) => {
onChange(value.toString());
setOpen(false);
}}
aria-label={t(label!)}
>
{clients}
</Select>
)}
/>
</FormGroup>
<ClientSelect
{...props}
name={`config.${props.name}`}
namespace="dynamic"
/>
);
};

View file

@ -0,0 +1,130 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
SelectOption,
FormGroup,
Select,
SelectVariant,
} from "@patternfly/react-core";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import type { UserQuery } from "@keycloak/keycloak-admin-client/lib/resources/users";
import type { ComponentProps } from "../dynamic/components";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { HelpItem } from "../help-enabler/HelpItem";
import useToggle from "../../utils/useToggle";
type UserSelectProps = ComponentProps & {
variant?: SelectVariant;
isRequired?: boolean;
};
export const UserSelect = ({
name,
label,
helpText,
defaultValue,
isRequired,
variant = SelectVariant.typeaheadMulti,
}: UserSelectProps) => {
const { t } = useTranslation("clients");
const {
control,
getValues,
formState: { errors },
} = useFormContext();
const values: string[] | undefined = getValues(name!);
const [open, toggleOpen] = useToggle();
const [users, setUsers] = useState<(UserRepresentation | undefined)[]>([]);
const [search, setSearch] = useState("");
const adminClient = useAdminClient();
useFetch(
() => {
const params: UserQuery = {
max: 20,
};
if (search) {
params.name = search;
}
if (values?.length && !search) {
return Promise.all(
values.map((id: string) => adminClient.users.findOne({ id }))
);
}
return adminClient.users.find(params);
},
setUsers,
[search]
);
const convert = (clients: (UserRepresentation | undefined)[]) =>
clients
.filter((c) => c !== undefined)
.map((option) => (
<SelectOption
key={option!.id}
value={option!.id}
selected={values?.includes(option!.id!)}
>
{option!.username}
</SelectOption>
));
return (
<FormGroup
label={t(label!)}
isRequired={isRequired}
labelIcon={
<HelpItem helpText={helpText!} fieldLabelId={`clients:${label}`} />
}
fieldId={name!}
validated={errors[name!] ? "error" : "default"}
helperTextInvalid={t("common:required")}
>
<Controller
name={name!}
defaultValue={defaultValue}
control={control}
rules={
isRequired && variant === SelectVariant.typeaheadMulti
? { validate: (value) => value.length > 0 }
: isRequired
? { required: true }
: {}
}
render={({ onChange, value }) => (
<Select
toggleId={name!}
variant={variant}
placeholderText={t("selectAUser")}
onToggle={toggleOpen}
isOpen={open}
selections={value}
onFilter={(_, value) => {
setSearch(value);
return convert(users);
}}
onSelect={(_, v) => {
const option = v.toString();
if (value.includes(option)) {
onChange(value.filter((item: string) => item !== option));
} else {
onChange([...value, option]);
}
toggleOpen();
}}
aria-label={t(name!)}
>
{convert(users)}
</Select>
)}
/>
</FormGroup>
);
};