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:
Erik Jan de Wit 2022-02-10 09:23:23 +01:00 committed by GitHub
parent 5968e1a7ee
commit 5fd344a7bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 186 additions and 51 deletions

View file

@ -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"

View file

@ -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>

View 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>
)}
/>
);
};

View file

@ -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: