auth evaluate wip wip auth evaluate tab add identity information and permissions help text and wip evaluate wip contextual attributes wip contextual attributes add conditional dropdown for auth method wip resource fields scopes and context inputs working fix resources error and update onChange package-lock cleanup: remove comments and log stmts add conditional fields when applyToResourceType is true package.json from main PR feedback from Erik Co-authored-by: Erik Jan de Wit <edewit@redhat.com> Update src/clients/authorization/AuthorizationEvaluate.tsx Co-authored-by: Erik Jan de Wit <edewit@redhat.com> handleSubmit remove log stmt fix cypress test PR feedback from Erik try fixing policies test PR feedback from Jon Co-authored-by: Jon Koops <jonkoops@gmail.com> PR feedback from Jon rename id revert client policy test reset add trigger authEvaluateReset conditionally render based on type Apply suggestions from code review Co-authored-by: Jon Koops <jonkoops@gmail.com> PR feedback remove controller Update src/components/attribute-input/AttributeInput.tsx Co-authored-by: Jon Koops <jonkoops@gmail.com> PR feedback Update src/clients/authorization/AuthorizationEvaluate.tsx Co-authored-by: Jon Koops <jonkoops@gmail.com> remove reset
This commit is contained in:
parent
a9e67b5fc4
commit
158bd07398
5 changed files with 110 additions and 72 deletions
|
@ -598,7 +598,6 @@ export default function ClientDetails() {
|
||||||
clientRoles={clientRoles}
|
clientRoles={clientRoles}
|
||||||
users={users}
|
users={users}
|
||||||
save={save}
|
save={save}
|
||||||
reset={() => setupForm(client)}
|
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
|
|
|
@ -28,6 +28,18 @@ import { defaultContextAttributes } from "../utils";
|
||||||
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
|
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
|
import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
|
||||||
|
import type { KeyValueType } from "../../components/attribute-form/attribute-convert";
|
||||||
|
|
||||||
|
interface EvaluateFormInputs
|
||||||
|
extends Omit<ResourceEvaluation, "context" | "resources"> {
|
||||||
|
applyToResource: boolean;
|
||||||
|
alias: string;
|
||||||
|
authScopes: string[];
|
||||||
|
context: {
|
||||||
|
attributes: Record<string, string>[];
|
||||||
|
};
|
||||||
|
resources: Record<string, string>[];
|
||||||
|
}
|
||||||
|
|
||||||
export type AttributeType = {
|
export type AttributeType = {
|
||||||
key: string;
|
key: string;
|
||||||
|
@ -42,20 +54,28 @@ type ClientSettingsProps = {
|
||||||
clients: ClientRepresentation[];
|
clients: ClientRepresentation[];
|
||||||
clientName?: string;
|
clientName?: string;
|
||||||
save: () => void;
|
save: () => void;
|
||||||
reset: () => void;
|
|
||||||
users: UserRepresentation[];
|
users: UserRepresentation[];
|
||||||
clientRoles: RoleRepresentation[];
|
clientRoles: RoleRepresentation[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AttributeForm = Omit<
|
||||||
|
EvaluateFormInputs,
|
||||||
|
"context" | "resources"
|
||||||
|
> & {
|
||||||
|
context: {
|
||||||
|
attributes?: KeyValueType[];
|
||||||
|
};
|
||||||
|
resources?: KeyValueType[];
|
||||||
|
};
|
||||||
|
|
||||||
export const AuthorizationEvaluate = ({
|
export const AuthorizationEvaluate = ({
|
||||||
clients,
|
clients,
|
||||||
clientRoles,
|
clientRoles,
|
||||||
clientName,
|
clientName,
|
||||||
users,
|
users,
|
||||||
reset,
|
|
||||||
}: ClientSettingsProps) => {
|
}: ClientSettingsProps) => {
|
||||||
const form = useFormContext<ResourceEvaluation>();
|
const form = useFormContext<EvaluateFormInputs>();
|
||||||
const { control } = form;
|
const { control, reset, trigger } = form;
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const realm = useRealm();
|
const realm = useRealm();
|
||||||
|
@ -90,15 +110,27 @@ export const AuthorizationEvaluate = ({
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const evaluate = (formValues: ResourceEvaluation) => {
|
const evaluate = async () => {
|
||||||
|
if (!(await trigger())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formValues = form.getValues();
|
||||||
|
const keys = formValues.resources.map(({ key }) => key);
|
||||||
const resEval: ResourceEvaluation = {
|
const resEval: ResourceEvaluation = {
|
||||||
roleIds: formValues.roleIds ?? [],
|
roleIds: formValues.roleIds ?? [],
|
||||||
|
clientId: selectedClient ? selectedClient.id! : clientId,
|
||||||
userId: selectedUser?.id!,
|
userId: selectedUser?.id!,
|
||||||
|
resources: resources.filter((resource) => keys.includes(resource.name!)),
|
||||||
entitlements: false,
|
entitlements: false,
|
||||||
context: formValues.context,
|
context: {
|
||||||
resources: formValues.resources,
|
attributes: Object.fromEntries(
|
||||||
clientId: selectedClient?.id!,
|
formValues.context.attributes
|
||||||
|
.filter((item) => item.key || item.value !== "")
|
||||||
|
.map(({ key, value }) => [key, value])
|
||||||
|
),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return adminClient.clients.evaluateResource(
|
return adminClient.clients.evaluateResource(
|
||||||
{ id: clientId!, realm: realm.realm },
|
{ id: clientId!, realm: realm.realm },
|
||||||
resEval
|
resEval
|
||||||
|
@ -128,7 +160,10 @@ export const AuthorizationEvaluate = ({
|
||||||
fieldId="client"
|
fieldId="client"
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="client"
|
name="clientId"
|
||||||
|
rules={{
|
||||||
|
validate: (value) => value.length > 0,
|
||||||
|
}}
|
||||||
defaultValue={clientName}
|
defaultValue={clientName}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
|
@ -140,7 +175,7 @@ export const AuthorizationEvaluate = ({
|
||||||
onChange((value as ClientRepresentation).clientId);
|
onChange((value as ClientRepresentation).clientId);
|
||||||
setClientsDropdownOpen(false);
|
setClientsDropdownOpen(false);
|
||||||
}}
|
}}
|
||||||
selections={value}
|
selections={selectedClient === value ? value : clientName}
|
||||||
variant={SelectVariant.typeahead}
|
variant={SelectVariant.typeahead}
|
||||||
aria-label={t("client")}
|
aria-label={t("client")}
|
||||||
isOpen={clientsDropdownOpen}
|
isOpen={clientsDropdownOpen}
|
||||||
|
@ -171,6 +206,9 @@ export const AuthorizationEvaluate = ({
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="userId"
|
name="userId"
|
||||||
|
rules={{
|
||||||
|
validate: (value) => value.length > 0,
|
||||||
|
}}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
|
@ -212,7 +250,7 @@ export const AuthorizationEvaluate = ({
|
||||||
fieldId="realmRole"
|
fieldId="realmRole"
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="rolesIds"
|
name="roleIds"
|
||||||
placeholderText={t("selectARole")}
|
placeholderText={t("selectARole")}
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={[]}
|
defaultValue={[]}
|
||||||
|
@ -297,7 +335,10 @@ export const AuthorizationEvaluate = ({
|
||||||
fieldId={name!}
|
fieldId={name!}
|
||||||
>
|
>
|
||||||
<AttributeInput
|
<AttributeInput
|
||||||
selectableValues={resources.map((item) => item.name!)}
|
selectableValues={resources.map<AttributeType>((item) => ({
|
||||||
|
name: item.name!,
|
||||||
|
key: item._id!,
|
||||||
|
}))}
|
||||||
resources={resources}
|
resources={resources}
|
||||||
isKeySelectable
|
isKeySelectable
|
||||||
name="resources"
|
name="resources"
|
||||||
|
@ -390,35 +431,33 @@ export const AuthorizationEvaluate = ({
|
||||||
fieldId={name!}
|
fieldId={name!}
|
||||||
>
|
>
|
||||||
<AttributeInput
|
<AttributeInput
|
||||||
selectableValues={defaultContextAttributes.map(
|
selectableValues={defaultContextAttributes}
|
||||||
(item) => item.name
|
|
||||||
)}
|
|
||||||
isKeySelectable
|
isKeySelectable
|
||||||
name="context"
|
name="context.attributes"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</ExpandableSection>
|
</ExpandableSection>
|
||||||
<ActionGroup>
|
|
||||||
<Button data-testid="authorization-eval" type="submit">
|
|
||||||
{t("evaluate")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
data-testid="authorization-revert"
|
|
||||||
variant="link"
|
|
||||||
onClick={reset}
|
|
||||||
>
|
|
||||||
{t("common:revert")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
data-testid="authorization-revert"
|
|
||||||
variant="primary"
|
|
||||||
onClick={reset}
|
|
||||||
isDisabled
|
|
||||||
>
|
|
||||||
{t("lastEvaluation")}
|
|
||||||
</Button>
|
|
||||||
</ActionGroup>
|
|
||||||
</FormAccess>
|
</FormAccess>
|
||||||
|
<ActionGroup>
|
||||||
|
<Button data-testid="authorization-eval" onClick={() => evaluate()}>
|
||||||
|
{t("evaluate")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
data-testid="authorization-revert"
|
||||||
|
variant="link"
|
||||||
|
onClick={() => reset()}
|
||||||
|
>
|
||||||
|
{t("common:revert")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
data-testid="authorization-revert"
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => reset()}
|
||||||
|
isDisabled
|
||||||
|
>
|
||||||
|
{t("lastEvaluation")}
|
||||||
|
</Button>
|
||||||
|
</ActionGroup>
|
||||||
</FormPanel>
|
</FormPanel>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,7 +5,10 @@ import { ActionGroup, Button } from "@patternfly/react-core";
|
||||||
|
|
||||||
import type { RoleRepresentation } from "../../model/role-model";
|
import type { RoleRepresentation } from "../../model/role-model";
|
||||||
import type { KeyValueType } from "./attribute-convert";
|
import type { KeyValueType } from "./attribute-convert";
|
||||||
import { AttributeInput } from "../attribute-input/AttributeInput";
|
import {
|
||||||
|
AttributeInput,
|
||||||
|
AttributeType,
|
||||||
|
} from "../attribute-input/AttributeInput";
|
||||||
import { FormAccess } from "../form-access/FormAccess";
|
import { FormAccess } from "../form-access/FormAccess";
|
||||||
|
|
||||||
export type AttributeForm = Omit<RoleRepresentation, "attributes"> & {
|
export type AttributeForm = Omit<RoleRepresentation, "attributes"> & {
|
||||||
|
@ -15,7 +18,7 @@ export type AttributeForm = Omit<RoleRepresentation, "attributes"> & {
|
||||||
export type AttributesFormProps = {
|
export type AttributesFormProps = {
|
||||||
form: UseFormMethods<AttributeForm>;
|
form: UseFormMethods<AttributeForm>;
|
||||||
isKeySelectable?: boolean;
|
isKeySelectable?: boolean;
|
||||||
selectableValues?: string[];
|
selectableValues?: AttributeType[];
|
||||||
save?: (model: AttributeForm) => void;
|
save?: (model: AttributeForm) => void;
|
||||||
reset?: () => void;
|
reset?: () => void;
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { camelCase } from "lodash-es";
|
||||||
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
|
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
|
||||||
|
|
||||||
export type AttributeType = {
|
export type AttributeType = {
|
||||||
key: string;
|
key?: string;
|
||||||
name: string;
|
name: string;
|
||||||
custom?: boolean;
|
custom?: boolean;
|
||||||
values?: {
|
values?: {
|
||||||
|
@ -34,7 +34,7 @@ export type AttributeType = {
|
||||||
|
|
||||||
type AttributeInputProps = {
|
type AttributeInputProps = {
|
||||||
name: string;
|
name: string;
|
||||||
selectableValues?: string[];
|
selectableValues?: AttributeType[];
|
||||||
isKeySelectable?: boolean;
|
isKeySelectable?: boolean;
|
||||||
resources?: ResourceRepresentation[];
|
resources?: ResourceRepresentation[];
|
||||||
};
|
};
|
||||||
|
@ -56,7 +56,7 @@ export const AttributeInput = ({
|
||||||
if (!fields.length) {
|
if (!fields.length) {
|
||||||
append({ key: "", value: "" });
|
append({ key: "", value: "" });
|
||||||
}
|
}
|
||||||
}, []);
|
}, [fields]);
|
||||||
|
|
||||||
const [isKeyOpenArray, setIsKeyOpenArray] = useState([false]);
|
const [isKeyOpenArray, setIsKeyOpenArray] = useState([false]);
|
||||||
const watchLastKey = watch(`${name}[${fields.length - 1}].key`, "");
|
const watchLastKey = watch(`${name}[${fields.length - 1}].key`, "");
|
||||||
|
@ -84,16 +84,32 @@ export const AttributeInput = ({
|
||||||
|
|
||||||
if (selectableValues) {
|
if (selectableValues) {
|
||||||
attributeValues = defaultContextAttributes.find(
|
attributeValues = defaultContextAttributes.find(
|
||||||
(attr) => attr.name === getValues().context[rowIndex]?.key
|
(attr) => attr.key === getValues().context[rowIndex]?.key
|
||||||
)?.values;
|
)?.values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderSelectOptionType = () => {
|
||||||
|
if (attributeValues?.length && !resources) {
|
||||||
|
return attributeValues.map((attr) => (
|
||||||
|
<SelectOption key={attr.key} value={attr.key}>
|
||||||
|
{attr.name}
|
||||||
|
</SelectOption>
|
||||||
|
));
|
||||||
|
} else if (scopeValues?.length) {
|
||||||
|
return scopeValues.map((scope) => (
|
||||||
|
<SelectOption key={scope.name} value={scope.name}>
|
||||||
|
{scope.name}
|
||||||
|
</SelectOption>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getMessageBundleKey = (attributeName: string) =>
|
const getMessageBundleKey = (attributeName: string) =>
|
||||||
camelCase(attributeName).replace(/\W/g, "");
|
camelCase(attributeName).replace(/\W/g, "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Td>
|
<Td>
|
||||||
{scopeValues?.length || attributeValues?.length ? (
|
{resources || attributeValues?.length ? (
|
||||||
<Controller
|
<Controller
|
||||||
name={`${name}[${rowIndex}].value`}
|
name={`${name}[${rowIndex}].value`}
|
||||||
defaultValue={[]}
|
defaultValue={[]}
|
||||||
|
@ -111,31 +127,17 @@ export const AttributeInput = ({
|
||||||
toggleId={`group-${name}`}
|
toggleId={`group-${name}`}
|
||||||
onToggle={(open) => toggleValueSelect(rowIndex, open)}
|
onToggle={(open) => toggleValueSelect(rowIndex, open)}
|
||||||
isOpen={isValueOpenArray[rowIndex]}
|
isOpen={isValueOpenArray[rowIndex]}
|
||||||
variant={
|
variant={SelectVariant.typeahead}
|
||||||
resources
|
|
||||||
? SelectVariant.typeaheadMulti
|
|
||||||
: SelectVariant.typeahead
|
|
||||||
}
|
|
||||||
typeAheadAriaLabel={t("clients:selectOrTypeAKey")}
|
typeAheadAriaLabel={t("clients:selectOrTypeAKey")}
|
||||||
placeholderText={t("clients:selectOrTypeAKey")}
|
placeholderText={t("clients:selectOrTypeAKey")}
|
||||||
selections={value}
|
selections={value}
|
||||||
onSelect={(_, v) => {
|
onSelect={(_, v) => {
|
||||||
if (resources) {
|
onChange(v);
|
||||||
const option = v.toString();
|
|
||||||
if (value.includes(option)) {
|
|
||||||
onChange(value.filter((item: string) => item !== option));
|
|
||||||
} else {
|
|
||||||
onChange([...value, option]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onChange(v);
|
|
||||||
}
|
|
||||||
toggleValueSelect(rowIndex, false);
|
toggleValueSelect(rowIndex, false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(scopeValues || attributeValues)?.map((scope) => (
|
{renderSelectOptionType()}
|
||||||
<SelectOption key={scope.name} value={scope.name} />
|
|
||||||
))}
|
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -192,18 +194,18 @@ export const AttributeInput = ({
|
||||||
placeholderText={t("clients:selectOrTypeAKey")}
|
placeholderText={t("clients:selectOrTypeAKey")}
|
||||||
selections={value}
|
selections={value}
|
||||||
onSelect={(_, v) => {
|
onSelect={(_, v) => {
|
||||||
onChange(v);
|
onChange(v.toString());
|
||||||
|
|
||||||
toggleKeySelect(rowIndex, false);
|
toggleKeySelect(rowIndex, false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectableValues?.map((attribute) => (
|
{selectableValues?.map((attribute) => (
|
||||||
<SelectOption
|
<SelectOption
|
||||||
selected={attribute === value}
|
selected={attribute.name === value}
|
||||||
key={attribute}
|
key={attribute.key}
|
||||||
value={attribute}
|
value={resources ? attribute.name : attribute.key}
|
||||||
>
|
>
|
||||||
{attribute}
|
{attribute.name}
|
||||||
</SelectOption>
|
</SelectOption>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
|
|
@ -39,7 +39,6 @@ import {
|
||||||
ClientRoleRoute,
|
ClientRoleRoute,
|
||||||
toClientRole,
|
toClientRole,
|
||||||
} from "./routes/ClientRole";
|
} from "./routes/ClientRole";
|
||||||
import { defaultContextAttributes } from "../clients/utils";
|
|
||||||
|
|
||||||
export default function RealmRoleTabs() {
|
export default function RealmRoleTabs() {
|
||||||
const { t } = useTranslation("roles");
|
const { t } = useTranslation("roles");
|
||||||
|
@ -378,10 +377,6 @@ export default function RealmRoleTabs() {
|
||||||
title={<TabTitleText>{t("common:attributes")}</TabTitleText>}
|
title={<TabTitleText>{t("common:attributes")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<AttributesForm
|
<AttributesForm
|
||||||
isKeySelectable
|
|
||||||
selectableValues={defaultContextAttributes.map(
|
|
||||||
(item) => item.key
|
|
||||||
)}
|
|
||||||
form={form}
|
form={form}
|
||||||
save={save}
|
save={save}
|
||||||
reset={() => reset(role)}
|
reset={() => reset(role)}
|
||||||
|
|
Loading…
Reference in a new issue