Added scope typed permission screen (#1933)
* added scope type permission * review comments * Update src/clients/authorization/PermissionDetails.tsx Co-authored-by: Jon Koops <jonkoops@gmail.com> * Update src/clients/authorization/ScopeSelect.tsx Co-authored-by: Jon Koops <jonkoops@gmail.com> * PR review Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
parent
5968e1a7ee
commit
5fd344a7bd
4 changed files with 186 additions and 51 deletions
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Link, useHistory, useParams } from "react-router-dom";
|
import { Link, useHistory, useParams } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm, useWatch } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
ActionGroup,
|
ActionGroup,
|
||||||
AlertVariant,
|
AlertVariant,
|
||||||
|
@ -11,6 +11,7 @@ import {
|
||||||
FormGroup,
|
FormGroup,
|
||||||
PageSection,
|
PageSection,
|
||||||
Radio,
|
Radio,
|
||||||
|
SelectVariant,
|
||||||
Switch,
|
Switch,
|
||||||
TextArea,
|
TextArea,
|
||||||
TextInput,
|
TextInput,
|
||||||
|
@ -30,13 +31,14 @@ import { useAlerts } from "../../components/alert/Alerts";
|
||||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||||
import { ResourcesPolicySelect } from "./ResourcesPolicySelect";
|
import { ResourcesPolicySelect } from "./ResourcesPolicySelect";
|
||||||
import { toAuthorizationTab } from "../routes/AuthenticationTab";
|
import { toAuthorizationTab } from "../routes/AuthenticationTab";
|
||||||
|
import { ScopeSelect } from "./ScopeSelect";
|
||||||
|
|
||||||
const DECISION_STRATEGIES = ["UNANIMOUS", "AFFIRMATIVE", "CONSENSUS"] as const;
|
const DECISION_STRATEGIES = ["UNANIMOUS", "AFFIRMATIVE", "CONSENSUS"] as const;
|
||||||
|
|
||||||
export default function PermissionDetails() {
|
export default function PermissionDetails() {
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm<PolicyRepresentation>({
|
||||||
shouldUnregister: false,
|
shouldUnregister: false,
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
});
|
});
|
||||||
|
@ -54,37 +56,42 @@ export default function PermissionDetails() {
|
||||||
|
|
||||||
useFetch(
|
useFetch(
|
||||||
async () => {
|
async () => {
|
||||||
if (permissionId) {
|
if (!permissionId) {
|
||||||
const r = await Promise.all([
|
return {};
|
||||||
adminClient.clients.findOnePermission({
|
|
||||||
id,
|
|
||||||
type: permissionType,
|
|
||||||
permissionId,
|
|
||||||
}),
|
|
||||||
adminClient.clients.getAssociatedResources({
|
|
||||||
id,
|
|
||||||
permissionId,
|
|
||||||
}),
|
|
||||||
adminClient.clients.getAssociatedPolicies({
|
|
||||||
id,
|
|
||||||
permissionId,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!r[0]) {
|
|
||||||
throw new Error(t("common:notFound"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
permission: r[0],
|
|
||||||
resources: r[1].map((p) => p._id),
|
|
||||||
policies: r[2].map((p) => p.id!),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return {};
|
const [permission, resources, policies, scopes] = await Promise.all([
|
||||||
|
adminClient.clients.findOnePermission({
|
||||||
|
id,
|
||||||
|
type: permissionType,
|
||||||
|
permissionId,
|
||||||
|
}),
|
||||||
|
adminClient.clients.getAssociatedResources({
|
||||||
|
id,
|
||||||
|
permissionId,
|
||||||
|
}),
|
||||||
|
adminClient.clients.getAssociatedPolicies({
|
||||||
|
id,
|
||||||
|
permissionId,
|
||||||
|
}),
|
||||||
|
adminClient.clients.getAssociatedScopes({
|
||||||
|
id,
|
||||||
|
permissionId,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!permission) {
|
||||||
|
throw new Error(t("common:notFound"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
permission,
|
||||||
|
resources: resources.map((r) => r._id),
|
||||||
|
policies: policies.map((p) => p.id!),
|
||||||
|
scopes: scopes.map((s) => s.id!),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
({ permission, resources, policies }) => {
|
({ permission, resources, policies, scopes }) => {
|
||||||
reset({ ...permission, resources, policies });
|
reset({ ...permission, resources, policies, scopes });
|
||||||
if (permission && "resourceType" in permission) {
|
if (permission && "resourceType" in permission) {
|
||||||
setApplyToResourceTypeFlag(
|
setApplyToResourceTypeFlag(
|
||||||
!!(permission as { resourceType: string }).resourceType
|
!!(permission as { resourceType: string }).resourceType
|
||||||
|
@ -149,6 +156,12 @@ export default function PermissionDetails() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const resourcesIds = useWatch<PolicyRepresentation["resources"]>({
|
||||||
|
control,
|
||||||
|
name: "resources",
|
||||||
|
defaultValue: [],
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteConfirm />
|
<DeleteConfirm />
|
||||||
|
@ -211,25 +224,27 @@ export default function PermissionDetails() {
|
||||||
validated={errors.description ? "error" : "default"}
|
validated={errors.description ? "error" : "default"}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup
|
{permissionType !== "scope" && (
|
||||||
label={t("applyToResourceTypeFlag")}
|
<FormGroup
|
||||||
fieldId="applyToResourceTypeFlag"
|
label={t("applyToResourceTypeFlag")}
|
||||||
labelIcon={
|
fieldId="applyToResourceTypeFlag"
|
||||||
<HelpItem
|
labelIcon={
|
||||||
helpText="clients-help:applyToResourceTypeFlag"
|
<HelpItem
|
||||||
fieldLabelId="clients:applyToResourceTypeFlag"
|
helpText="clients-help:applyToResourceTypeFlag"
|
||||||
|
fieldLabelId="clients:applyToResourceTypeFlag"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
id="applyToResourceTypeFlag"
|
||||||
|
name="applyToResourceTypeFlag"
|
||||||
|
label={t("common:on")}
|
||||||
|
labelOff={t("common:off")}
|
||||||
|
isChecked={applyToResourceTypeFlag}
|
||||||
|
onChange={setApplyToResourceTypeFlag}
|
||||||
/>
|
/>
|
||||||
}
|
</FormGroup>
|
||||||
>
|
)}
|
||||||
<Switch
|
|
||||||
id="applyToResourceTypeFlag"
|
|
||||||
name="applyToResourceTypeFlag"
|
|
||||||
label={t("common:on")}
|
|
||||||
labelOff={t("common:off")}
|
|
||||||
isChecked={applyToResourceTypeFlag}
|
|
||||||
onChange={setApplyToResourceTypeFlag}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
{applyToResourceTypeFlag ? (
|
{applyToResourceTypeFlag ? (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t("resourceType")}
|
label={t("resourceType")}
|
||||||
|
@ -262,9 +277,31 @@ export default function PermissionDetails() {
|
||||||
name="resources"
|
name="resources"
|
||||||
searchFunction="listResources"
|
searchFunction="listResources"
|
||||||
clientId={id}
|
clientId={id}
|
||||||
|
variant={
|
||||||
|
permissionType === "scope"
|
||||||
|
? SelectVariant.typeahead
|
||||||
|
: SelectVariant.typeaheadMulti
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
)}
|
)}
|
||||||
|
{permissionType === "scope" && (
|
||||||
|
<FormGroup
|
||||||
|
label={t("authorizationScopes")}
|
||||||
|
fieldId="scopes"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:permissionScopes"
|
||||||
|
fieldLabelId="clients:scopesSelect"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helperTextInvalid={t("common:required")}
|
||||||
|
validated={errors.scopes ? "error" : "default"}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<ScopeSelect clientId={id} resourceId={resourcesIds?.[0]} />
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t("policies")}
|
label={t("policies")}
|
||||||
fieldId="policies"
|
fieldId="policies"
|
||||||
|
|
|
@ -11,12 +11,14 @@ type ResourcesPolicySelectProps = {
|
||||||
name: string;
|
name: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
searchFunction: keyof Pick<Clients, "listPolicies" | "listResources">;
|
searchFunction: keyof Pick<Clients, "listPolicies" | "listResources">;
|
||||||
|
variant?: SelectVariant;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ResourcesPolicySelect = ({
|
export const ResourcesPolicySelect = ({
|
||||||
name,
|
name,
|
||||||
searchFunction,
|
searchFunction,
|
||||||
clientId,
|
clientId,
|
||||||
|
variant = SelectVariant.typeaheadMulti,
|
||||||
}: ResourcesPolicySelectProps) => {
|
}: ResourcesPolicySelectProps) => {
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
|
@ -58,7 +60,7 @@ export const ResourcesPolicySelect = ({
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<Select
|
<Select
|
||||||
toggleId={name}
|
toggleId={name}
|
||||||
variant={SelectVariant.typeaheadMulti}
|
variant={variant}
|
||||||
onToggle={setOpen}
|
onToggle={setOpen}
|
||||||
onFilter={(_, filter) => {
|
onFilter={(_, filter) => {
|
||||||
setSearch(filter);
|
setSearch(filter);
|
||||||
|
@ -78,7 +80,7 @@ export const ResourcesPolicySelect = ({
|
||||||
setSearch("");
|
setSearch("");
|
||||||
}}
|
}}
|
||||||
isOpen={open}
|
isOpen={open}
|
||||||
aria-labelledby={t("policies")}
|
aria-labelledby={t(name)}
|
||||||
>
|
>
|
||||||
{items}
|
{items}
|
||||||
</Select>
|
</Select>
|
||||||
|
|
94
src/clients/authorization/ScopeSelect.tsx
Normal file
94
src/clients/authorization/ScopeSelect.tsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import { Select, SelectOption, SelectVariant } from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
|
||||||
|
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||||
|
|
||||||
|
type ScopeSelectProps = {
|
||||||
|
clientId: string;
|
||||||
|
resourceId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ScopeSelect = ({ clientId, resourceId }: ScopeSelectProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
|
||||||
|
const { control, errors, setValue } = useFormContext();
|
||||||
|
|
||||||
|
const [scopes, setScopes] = useState<ScopeRepresentation[]>([]);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const firstUpdate = useRef(true);
|
||||||
|
|
||||||
|
const toSelectOptions = (scopes: ScopeRepresentation[]) =>
|
||||||
|
scopes.map((scope) => (
|
||||||
|
<SelectOption key={scope.id} value={scope.id}>
|
||||||
|
{scope.name}
|
||||||
|
</SelectOption>
|
||||||
|
));
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
async () => {
|
||||||
|
if (!resourceId) {
|
||||||
|
return adminClient.clients.listAllScopes(
|
||||||
|
Object.assign(
|
||||||
|
{ id: clientId, first: 0, max: 10, deep: false },
|
||||||
|
search === "" ? null : { name: search }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceId && !firstUpdate.current) {
|
||||||
|
setValue("scopes", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdate.current = false;
|
||||||
|
return adminClient.clients.listScopesByResource({
|
||||||
|
id: clientId,
|
||||||
|
resourceName: resourceId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setScopes,
|
||||||
|
[resourceId, search]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name="scopes"
|
||||||
|
defaultValue={[]}
|
||||||
|
control={control}
|
||||||
|
rules={{ validate: (value) => value.length > 0 }}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Select
|
||||||
|
toggleId="scopes"
|
||||||
|
variant={SelectVariant.typeaheadMulti}
|
||||||
|
onToggle={setOpen}
|
||||||
|
onFilter={(_, filter) => {
|
||||||
|
setSearch(filter);
|
||||||
|
return toSelectOptions(scopes);
|
||||||
|
}}
|
||||||
|
onClear={() => {
|
||||||
|
onChange([]);
|
||||||
|
setSearch("");
|
||||||
|
}}
|
||||||
|
selections={value}
|
||||||
|
onSelect={(_, selectedValue) => {
|
||||||
|
const option = selectedValue.toString();
|
||||||
|
const changedValue = value.find((p: string) => p === option)
|
||||||
|
? value.filter((p: string) => p !== option)
|
||||||
|
: [...value, option];
|
||||||
|
onChange(changedValue);
|
||||||
|
setSearch("");
|
||||||
|
}}
|
||||||
|
isOpen={open}
|
||||||
|
aria-labelledby={t("scopes")}
|
||||||
|
validated={errors.scopes ? "error" : "default"}
|
||||||
|
>
|
||||||
|
{toSelectOptions(scopes)}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -228,6 +228,8 @@ export default {
|
||||||
"Specifies if this permission should be applied to all resources with a given type. In this case, this permission will be evaluated for all instances of a given resource type.",
|
"Specifies if this permission should be applied to all resources with a given type. In this case, this permission will be evaluated for all instances of a given resource type.",
|
||||||
permissionResources:
|
permissionResources:
|
||||||
"Specifies that this permission must be applied to a specific resource instance.",
|
"Specifies that this permission must be applied to a specific resource instance.",
|
||||||
|
permissionScopes:
|
||||||
|
"Specifies that this permission must be applied to one or more scopes.",
|
||||||
permissionType:
|
permissionType:
|
||||||
"Specifies that this permission must be applied to all resources instances of a given type.",
|
"Specifies that this permission must be applied to all resources instances of a given type.",
|
||||||
permissionDecisionStrategy:
|
permissionDecisionStrategy:
|
||||||
|
|
Loading…
Reference in a new issue