Support OR condition for forms + authz (#24879)

Closes: #24586

Signed-off-by: Hynek Mlnarik <hmlnarik@redhat.com>
This commit is contained in:
Hynek Mlnařík 2023-11-28 14:07:11 +01:00 committed by GitHub
parent ab3758842c
commit c03c2e953a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 389 additions and 160 deletions

View file

@ -195,6 +195,80 @@ describe("Client authentication subtab", () => {
);
});
describe("Client authorization tab access for view-realm-authorization", () => {
const clientId = "realm-view-authz-client-" + uuid();
beforeEach(async () => {
const [, testUser] = await Promise.all([
adminClient.createRealm("realm-view-authz"),
adminClient.createUser({
// Create user in master realm
username: "test-view-authz-user",
enabled: true,
credentials: [{ type: "password", value: "password" }],
}),
]);
await Promise.all([
adminClient.addClientRoleToUser(
testUser.id!,
"realm-view-authz-realm",
["view-realm", "view-users", "view-authorization", "view-clients"],
),
adminClient.createClient({
realm: "realm-view-authz",
clientId,
authorizationServicesEnabled: true,
serviceAccountsEnabled: true,
standardFlowEnabled: true,
}),
]);
});
after(() =>
Promise.all([
adminClient.deleteClient(clientId),
adminClient.deleteUser("test-view-authz-user"),
adminClient.deleteRealm("realm-view-authz"),
]),
);
it("Should view autorization tab", () => {
sidebarPage.waitForPageLoad();
masthead.signOut();
sidebarPage.waitForPageLoad();
loginPage.logIn("test-view-authz-user", "password");
keycloakBefore();
sidebarPage
.waitForPageLoad()
.goToRealm("realm-view-authz")
.waitForPageLoad()
.goToClients();
listingPage
.searchItem(clientId, true, "realm-view-authz")
.goToItemDetails(clientId);
clientDetailsPage.goToAuthorizationTab();
authenticationTab.goToResourcesSubTab();
sidebarPage.waitForPageLoad();
listingPage.goToItemDetails("Resource");
sidebarPage.waitForPageLoad();
cy.go("back");
authenticationTab.goToScopesSubTab();
sidebarPage.waitForPageLoad();
authenticationTab.goToPoliciesSubTab();
sidebarPage.waitForPageLoad();
authenticationTab.goToPermissionsSubTab();
sidebarPage.waitForPageLoad();
authenticationTab.goToEvaluateSubTab();
sidebarPage.waitForPageLoad();
});
});
describe("Accessibility tests for client authorization", () => {
beforeEach(() => {
loginPage.logIn();

View file

@ -89,9 +89,9 @@ export default class ListingPage extends CommonElements {
return this;
}
searchItem(searchValue: string, wait = true) {
searchItem(searchValue: string, wait = true, realm = "master") {
if (wait) {
const searchUrl = `/admin/realms/master/**/*${searchValue}*`;
const searchUrl = `/admin/realms/${realm}/**/*${searchValue}*`;
cy.intercept(searchUrl).as("search");
}

View file

@ -52,7 +52,11 @@ class AdminClient {
await this.#client.realms.del({ realm });
}
async createClient(client: ClientRepresentation) {
async createClient(
client: ClientRepresentation & {
realm?: string;
},
) {
await this.#login();
await this.#client.clients.create(client);
}
@ -68,6 +72,11 @@ class AdminClient {
}
}
async getClient(clientName: string) {
await this.#login();
return (await this.#client.clients.find({ clientId: clientName }))[0];
}
async createGroup(groupName: string) {
await this.#login();
return await this.#client.groups.create({ name: groupName });
@ -149,6 +158,30 @@ class AdminClient {
});
}
async addClientRoleToUser(
userId: string,
clientId: string,
roleNames: string[],
) {
await this.#login();
const client = await this.#client.clients.find({ clientId });
const clientRoles = await Promise.all(
roleNames.map(
async (roleName) =>
(await this.#client.clients.findRole({
id: client[0].id!,
roleName: roleName,
})) as RoleMappingPayload,
),
);
await this.#client.users.addClientRoleMappings({
id: userId,
clientUniqueId: client[0].id!,
roles: clientRoles,
});
}
async deleteUser(username: string) {
await this.#login();
const user = await this.#client.users.find({ username });

View file

@ -11,11 +11,14 @@ export const ForbiddenSection = ({
permissionNeeded,
}: ForbiddenSectionProps) => {
const { t } = useTranslation();
const count = Array.isArray(permissionNeeded) ? permissionNeeded.length : 1;
const permissionNeededArray = Array.isArray(permissionNeeded)
? permissionNeeded
: [permissionNeeded];
return (
<PageSection>
{t("forbidden", { count })} {permissionNeeded}
{t("forbidden", { count: permissionNeededArray.length })}{" "}
{permissionNeededArray.map((p) => p.toString())}
</PageSection>
);
};

View file

@ -195,11 +195,13 @@ export default function ClientDetails() {
const isFeatureEnabled = useIsFeatureEnabled();
const hasManageAuthorization = hasAccess("manage-authorization");
const hasViewAuthorization = hasAccess("view-authorization");
const hasManageClients = hasAccess("manage-clients");
const hasViewClients = hasAccess("view-clients");
const hasViewUsers = hasAccess("view-users");
const permissionsEnabled =
isFeatureEnabled(Feature.AdminFineGrainedAuthz) && hasManageAuthorization;
isFeatureEnabled(Feature.AdminFineGrainedAuthz) &&
(hasManageAuthorization || hasViewAuthorization);
const navigate = useNavigate();
@ -530,83 +532,98 @@ export default function ClientDetails() {
</RoutableTabs>
</Tab>
)}
{client!.authorizationServicesEnabled && hasManageAuthorization && (
<Tab
id="authorization"
data-testid="authorizationTab"
title={<TabTitleText>{t("authorization")}</TabTitleText>}
{...authorizationTab}
>
<RoutableTabs
mountOnEnter
unmountOnExit
defaultLocation={toAuthorizationTab({
realm,
clientId,
tab: "settings",
})}
{client!.authorizationServicesEnabled &&
(hasManageAuthorization || hasViewAuthorization) && (
<Tab
id="authorization"
data-testid="authorizationTab"
title={<TabTitleText>{t("authorization")}</TabTitleText>}
{...authorizationTab}
>
<Tab
id="settings"
data-testid="authorizationSettings"
title={<TabTitleText>{t("settings")}</TabTitleText>}
{...authorizationSettingsTab}
<RoutableTabs
mountOnEnter
unmountOnExit
defaultLocation={toAuthorizationTab({
realm,
clientId,
tab: "settings",
})}
>
<AuthorizationSettings clientId={clientId} />
</Tab>
<Tab
id="resources"
data-testid="authorizationResources"
title={<TabTitleText>{t("resources")}</TabTitleText>}
{...authorizationResourcesTab}
>
<AuthorizationResources clientId={clientId} />
</Tab>
<Tab
id="scopes"
data-testid="authorizationScopes"
title={<TabTitleText>{t("scopes")}</TabTitleText>}
{...authorizationScopesTab}
>
<AuthorizationScopes clientId={clientId} />
</Tab>
<Tab
id="policies"
data-testid="authorizationPolicies"
title={<TabTitleText>{t("policies")}</TabTitleText>}
{...authorizationPoliciesTab}
>
<AuthorizationPolicies clientId={clientId} />
</Tab>
<Tab
id="permissions"
data-testid="authorizationPermissions"
title={<TabTitleText>{t("permissions")}</TabTitleText>}
{...authorizationPermissionsTab}
>
<AuthorizationPermissions clientId={clientId} />
</Tab>
{hasViewUsers && (
<Tab
id="evaluate"
data-testid="authorizationEvaluate"
title={<TabTitleText>{t("evaluate")}</TabTitleText>}
{...authorizationEvaluateTab}
id="settings"
data-testid="authorizationSettings"
title={<TabTitleText>{t("settings")}</TabTitleText>}
{...authorizationSettingsTab}
>
<AuthorizationEvaluate client={client} save={save} />
<AuthorizationSettings clientId={clientId} />
</Tab>
)}
<Tab
id="export"
data-testid="authorizationExport"
title={<TabTitleText>{t("export")}</TabTitleText>}
{...authorizationExportTab}
>
<AuthorizationExport />
</Tab>
</RoutableTabs>
</Tab>
)}
<Tab
id="resources"
data-testid="authorizationResources"
title={<TabTitleText>{t("resources")}</TabTitleText>}
{...authorizationResourcesTab}
>
<AuthorizationResources
clientId={clientId}
isDisabled={!hasManageAuthorization}
/>
</Tab>
<Tab
id="scopes"
data-testid="authorizationScopes"
title={<TabTitleText>{t("scopes")}</TabTitleText>}
{...authorizationScopesTab}
>
<AuthorizationScopes
clientId={clientId}
isDisabled={!hasManageAuthorization}
/>
</Tab>
<Tab
id="policies"
data-testid="authorizationPolicies"
title={<TabTitleText>{t("policies")}</TabTitleText>}
{...authorizationPoliciesTab}
>
<AuthorizationPolicies
clientId={clientId}
isDisabled={!hasManageAuthorization}
/>
</Tab>
<Tab
id="permissions"
data-testid="authorizationPermissions"
title={<TabTitleText>{t("permissions")}</TabTitleText>}
{...authorizationPermissionsTab}
>
<AuthorizationPermissions
clientId={clientId}
isDisabled={!hasManageAuthorization}
/>
</Tab>
{hasViewUsers && (
<Tab
id="evaluate"
data-testid="authorizationEvaluate"
title={<TabTitleText>{t("evaluate")}</TabTitleText>}
{...authorizationEvaluateTab}
>
<AuthorizationEvaluate client={client} save={save} />
</Tab>
)}
{hasAccess("manage-authorization") && (
<Tab
id="export"
data-testid="authorizationExport"
title={<TabTitleText>{t("export")}</TabTitleText>}
{...authorizationExportTab}
>
<AuthorizationExport />
</Tab>
)}
</RoutableTabs>
</Tab>
)}
{client!.serviceAccountsEnabled && hasViewUsers && (
<Tab
id="serviceAccount"

View file

@ -8,11 +8,13 @@ const DECISION_STRATEGY = ["UNANIMOUS", "AFFIRMATIVE", "CONSENSUS"] as const;
type DecisionStrategySelectProps = {
helpLabel?: string;
isDisabled?: boolean;
isLimited?: boolean;
};
export const DecisionStrategySelect = ({
helpLabel,
isDisabled = false,
isLimited = false,
}: DecisionStrategySelectProps) => {
const { t } = useTranslation();
@ -46,6 +48,7 @@ export const DecisionStrategySelect = ({
key={strategy}
data-testid={strategy}
isChecked={field.value === strategy}
isDisabled={isDisabled}
name="decisionStrategy"
onChange={() => field.onChange(strategy)}
label={t(`decisionStrategies.${strategy}`)}

View file

@ -37,6 +37,7 @@ import {
} from "../routes/PermissionDetails";
import { ResourcesPolicySelect } from "./ResourcesPolicySelect";
import { ScopeSelect } from "./ScopeSelect";
import { useAccess } from "../../context/access/Access";
type FormFields = PolicyRepresentation & {
resourceType: string;
@ -64,6 +65,9 @@ export default function PermissionDetails() {
const { addAlert, addError } = useAlerts();
const [permission, setPermission] = useState<PolicyRepresentation>();
const [applyToResourceTypeFlag, setApplyToResourceTypeFlag] = useState(false);
const { hasAccess } = useAccess();
const isDisabled = !hasAccess("manage-authorization");
useFetch(
async () => {
@ -192,6 +196,7 @@ export default function PermissionDetails() {
<DropdownItem
key="delete"
data-testid="delete-resource"
isDisabled={isDisabled}
onClick={() => toggleDeleteDialog()}
>
{t("delete")}
@ -203,7 +208,7 @@ export default function PermissionDetails() {
<PageSection variant="light">
<FormAccess
isHorizontal
role="view-clients"
role="manage-authorization"
onSubmit={handleSubmit(save)}
>
<FormProvider {...form}>
@ -376,6 +381,7 @@ export default function PermissionDetails() {
key={strategy}
data-testid={strategy}
isChecked={field.value === strategy}
isDisabled={isDisabled}
name="decisionStrategies"
onChange={() => field.onChange(strategy)}
label={t(`decisionStrategies.${strategy}`)}

View file

@ -46,6 +46,7 @@ import "./permissions.css";
type PermissionsProps = {
clientId: string;
isDisabled?: boolean;
};
type ExpandablePolicyRepresentation = PolicyRepresentation & {
@ -66,7 +67,10 @@ const AssociatedPoliciesRenderer = ({
);
};
export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
export const AuthorizationPermissions = ({
clientId,
isDisabled = false,
}: PermissionsProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { addAlert, addError } = useAlerts();
@ -204,6 +208,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
toggle={
<DropdownToggle
onToggle={toggleCreate}
isDisabled={isDisabled}
isPrimary
data-testid="permissionCreateDropdown"
>
@ -215,7 +220,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
<DropdownItem
data-testid="create-resource"
key="createResourceBasedPermission"
isDisabled={disabledCreate?.resources}
isDisabled={isDisabled || disabledCreate?.resources}
component="button"
onClick={() =>
navigate(
@ -233,7 +238,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
<DropdownItem
data-testid="create-scope"
key="createScopeBasedPermission"
isDisabled={disabledCreate?.scopes}
isDisabled={isDisabled || disabledCreate?.scopes}
component="button"
onClick={() =>
navigate(
@ -366,8 +371,8 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
{noData && !searching && (
<EmptyPermissionsState
clientId={clientId}
isResourceEnabled={disabledCreate?.resources}
isScopeEnabled={disabledCreate?.scopes}
isResourceEnabled={!isDisabled && disabledCreate?.resources}
isScopeEnabled={!isDisabled && disabledCreate?.scopes}
/>
)}
{noData && searching && (

View file

@ -41,6 +41,7 @@ import { SearchDropdown, SearchForm } from "./SearchDropdown";
type PoliciesProps = {
clientId: string;
isDisabled?: boolean;
};
type ExpandablePolicyRepresentation = PolicyRepresentation & {
@ -61,7 +62,10 @@ const DependentPoliciesRenderer = ({
);
};
export const AuthorizationPolicies = ({ clientId }: PoliciesProps) => {
export const AuthorizationPolicies = ({
clientId,
isDisabled = false,
}: PoliciesProps) => {
const { t } = useTranslation();
const { addAlert, addError } = useAlerts();
const { realm } = useRealm();
@ -201,7 +205,11 @@ export const AuthorizationPolicies = ({ clientId }: PoliciesProps) => {
/>
</ToolbarItem>
<ToolbarItem>
<Button data-testid="createPolicy" onClick={toggleDialog}>
<Button
data-testid="createPolicy"
onClick={toggleDialog}
isDisabled={isDisabled}
>
{t("createPolicy")}
</Button>
</ToolbarItem>
@ -254,26 +262,28 @@ export const AuthorizationPolicies = ({ clientId }: PoliciesProps) => {
<DependentPoliciesRenderer row={policy} />
</Td>
<Td>{policy.description}</Td>
<Td
actions={{
items: [
{
title: t("delete"),
onClick: async () => {
setSelectedPolicy(policy);
toggleDeleteDialog();
{!isDisabled && (
<Td
actions={{
items: [
{
title: t("delete"),
onClick: () => {
setSelectedPolicy(policy);
toggleDeleteDialog();
},
},
},
],
}}
/>
],
}}
/>
)}
</Tr>
<Tr
key={`child-${policy.id}`}
isExpanded={policy.isExpanded}
>
<Td />
<Td colSpan={4}>
<Td colSpan={3 + (isDisabled ? 0 : 1)}>
<ExpandableRowContent>
{policy.isExpanded && (
<DescriptionList
@ -308,6 +318,7 @@ export const AuthorizationPolicies = ({ clientId }: PoliciesProps) => {
{noData && searching && (
<ListEmptyState
isSearchVariant
isDisabled={isDisabled}
message={t("noSearchResults")}
instructions={t("noSearchResultsInstructions")}
/>
@ -330,6 +341,7 @@ export const AuthorizationPolicies = ({ clientId }: PoliciesProps) => {
<ListEmptyState
message={t("emptyPolicies")}
instructions={t("emptyPoliciesInstructions")}
isDisabled={isDisabled}
primaryActionText={t("createPolicy")}
onPrimaryAction={toggleDialog}
/>

View file

@ -37,6 +37,7 @@ import { ResourceDetailsParams, toResourceDetails } from "../routes/Resource";
import { ScopePicker } from "./ScopePicker";
import "./resource-details.css";
import { useAccess } from "../../context/access/Access";
type SubmittedResource = Omit<
ResourceRepresentation,
@ -72,6 +73,10 @@ export default function ResourceDetails() {
convertToFormValues(resource, setValue);
};
const { hasAccess } = useAccess();
const isDisabled = !hasAccess("manage-authorization");
useFetch(
() =>
Promise.all([
@ -174,6 +179,7 @@ export default function ResourceDetails() {
<DropdownItem
key="delete"
data-testid="delete-resource"
isDisabled={isDisabled}
onClick={() => toggleDeleteDialog()}
>
{t("delete")}
@ -186,7 +192,7 @@ export default function ResourceDetails() {
<FormProvider {...form}>
<FormAccess
isHorizontal
role="view-clients"
role="manage-authorization"
className="keycloak__resource-details__form"
onSubmit={handleSubmit(submit)}
>
@ -316,7 +322,7 @@ export default function ResourceDetails() {
}
fieldId="resourceAttribute"
>
<KeyValueInput name="attributes" />
<KeyValueInput name="attributes" isDisabled={isDisabled} />
</FormGroup>
<ActionGroup>
<div className="pf-u-mt-md">

View file

@ -37,6 +37,7 @@ import { SearchDropdown, SearchForm } from "./SearchDropdown";
type ResourcesProps = {
clientId: string;
isDisabled?: boolean;
};
type ExpandableResourceRepresentation = ResourceRepresentation & {
@ -49,7 +50,10 @@ const UriRenderer = ({ row }: { row: ResourceRepresentation }) => (
</>
);
export const AuthorizationResources = ({ clientId }: ResourcesProps) => {
export const AuthorizationResources = ({
clientId,
isDisabled = false,
}: ResourcesProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { addAlert, addError } = useAlerts();
@ -168,6 +172,7 @@ export const AuthorizationResources = ({ clientId }: ResourcesProps) => {
<ToolbarItem>
<Button
data-testid="createResource"
isDisabled={isDisabled}
component={(props) => (
<Link
{...props}
@ -191,8 +196,12 @@ export const AuthorizationResources = ({ clientId }: ResourcesProps) => {
<Th>{t("type")}</Th>
<Th>{t("owner")}</Th>
<Th>{t("uris")}</Th>
<Th aria-hidden="true" />
<Th aria-hidden="true" />
{!isDisabled && (
<>
<Th aria-hidden="true" />
<Th aria-hidden="true" />
</>
)}
</Tr>
</Thead>
{resources.map((resource, rowIndex) => (
@ -232,40 +241,44 @@ export const AuthorizationResources = ({ clientId }: ResourcesProps) => {
<Td>
<UriRenderer row={resource} />
</Td>
<Td width={10}>
<Button
variant="link"
component={(props) => (
<Link
{...props}
to={toNewPermission({
realm,
id: clientId,
permissionType: "resource",
selectedId: resource._id,
})}
/>
)}
>
{t("createPermission")}
</Button>
</Td>
<Td
actions={{
items: [
{
title: t("delete"),
onClick: async () => {
setSelectedResource(resource);
setPermission(
await fetchPermissions(resource._id!),
);
toggleDeleteDialog();
},
},
],
}}
/>
{!isDisabled && (
<>
<Td width={10}>
<Button
variant="link"
component={(props) => (
<Link
{...props}
to={toNewPermission({
realm,
id: clientId,
permissionType: "resource",
selectedId: resource._id,
})}
/>
)}
>
{t("createPermission")}
</Button>
</Td>
<Td
actions={{
items: [
{
title: t("delete"),
onClick: async () => {
setSelectedResource(resource);
setPermission(
await fetchPermissions(resource._id!),
);
toggleDeleteDialog();
},
},
],
}}
/>
</>
)}
</Tr>
<Tr
key={`child-${resource._id}`}
@ -301,6 +314,7 @@ export const AuthorizationResources = ({ clientId }: ResourcesProps) => {
<ListEmptyState
message={t("emptyResources")}
instructions={t("emptyResourcesInstructions")}
isDisabled={isDisabled}
primaryActionText={t("createResource")}
onPrimaryAction={() =>
navigate(toCreateResource({ realm, id: clientId }))

View file

@ -125,7 +125,7 @@ export default function ScopeDetails() {
<PageSection variant="light">
<FormAccess
isHorizontal
role="view-clients"
role="manage-authorization"
onSubmit={handleSubmit(onSubmit)}
>
<FormGroup

View file

@ -36,6 +36,7 @@ import { DetailDescriptionLink } from "./DetailDescription";
type ScopesProps = {
clientId: string;
isDisabled?: boolean;
};
export type PermissionScopeRepresentation = ScopeRepresentation & {
@ -48,7 +49,10 @@ type ExpandableRow = {
isExpanded: boolean;
};
export const AuthorizationScopes = ({ clientId }: ScopesProps) => {
export const AuthorizationScopes = ({
clientId,
isDisabled = false,
}: ScopesProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { realm } = useRealm();
@ -305,6 +309,7 @@ export const AuthorizationScopes = ({ clientId }: ScopesProps) => {
<ListEmptyState
message={t("emptyAuthorizationScopes")}
instructions={t("emptyAuthorizationInstructions")}
isDisabled={isDisabled}
onPrimaryAction={() => navigate(toNewScope({ id: clientId, realm }))}
primaryActionText={t("createAuthorizationScope")}
/>
@ -312,6 +317,7 @@ export const AuthorizationScopes = ({ clientId }: ScopesProps) => {
{noData && searching && (
<ListEmptyState
isSearchVariant
isDisabled={isDisabled}
message={t("noSearchResults")}
instructions={t("noSearchResultsInstructions")}
/>

View file

@ -22,6 +22,7 @@ import useToggle from "../../utils/useToggle";
import { DecisionStrategySelect } from "./DecisionStrategySelect";
import { ImportDialog } from "./ImportDialog";
import { useFetch } from "../../utils/useFetch";
import { useAccess } from "../../context/access/Access";
const POLICY_ENFORCEMENT_MODES = [
"ENFORCING",
@ -43,6 +44,9 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
const { control, reset, handleSubmit } = form;
const { addAlert, addError } = useAlerts();
const { hasAccess } = useAccess();
const isDisabled = !hasAccess("manage-authorization");
useFetch(
() => adminClient.clients.getResourceServer({ id: clientId }),
@ -88,7 +92,7 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
/>
)}
<FormAccess
role="view-clients"
role="manage-authorization"
isHorizontal
onSubmit={handleSubmit(onSubmit)}
>
@ -128,6 +132,7 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
key={mode}
data-testid={mode}
isChecked={field.value === mode}
isDisabled={isDisabled}
name="policyEnforcementMode"
onChange={() => field.onChange(mode)}
label={t(`policyEnforcementModes.${mode}`)}

View file

@ -25,7 +25,8 @@ export const AuthorizationRoute: AppRouteObject = {
element: <ClientDetails />,
breadcrumb: (t) => t("clientSettings"),
handle: {
access: "manage-authorization",
access: (accessChecker) =>
accessChecker.hasAny("view-authorization", "manage-authorization"),
},
};

View file

@ -21,7 +21,8 @@ export const NewPermissionRoute: AppRouteObject = {
element: <PermissionDetails />,
breadcrumb: (t) => t("createPermission"),
handle: {
access: "view-clients",
access: (accessChecker) =>
accessChecker.hasAny("manage-clients", "manage-authorization"),
},
};

View file

@ -14,7 +14,8 @@ export const NewPolicyRoute: AppRouteObject = {
element: <PolicyDetails />,
breadcrumb: (t) => t("createPolicy"),
handle: {
access: "view-clients",
access: (accessChecker) =>
accessChecker.hasAny("manage-clients", "manage-authorization"),
},
};

View file

@ -12,7 +12,8 @@ export const NewResourceRoute: AppRouteObject = {
element: <ResourceDetails />,
breadcrumb: (t) => t("createResource"),
handle: {
access: "view-clients",
access: (accessChecker) =>
accessChecker.hasAny("manage-clients", "manage-authorization"),
},
};

View file

@ -12,7 +12,8 @@ export const NewScopeRoute: AppRouteObject = {
element: <ScopeDetails />,
breadcrumb: (t) => t("createAuthorizationScope"),
handle: {
access: "view-clients",
access: (accessChecker) =>
accessChecker.hasAny("manage-clients", "manage-authorization"),
},
};

View file

@ -20,7 +20,8 @@ export const PermissionDetailsRoute: AppRouteObject = {
element: <PermissionDetails />,
breadcrumb: (t) => t("permissionDetails"),
handle: {
access: "view-clients",
access: (accessChecker) =>
accessChecker.hasAny("manage-clients", "view-authorization"),
},
};

View file

@ -19,7 +19,8 @@ export const PolicyDetailsRoute: AppRouteObject = {
element: <PolicyDetails />,
breadcrumb: (t) => t("policyDetails"),
handle: {
access: "view-clients",
access: (accessChecker) =>
accessChecker.hasAny("manage-clients", "view-authorization"),
},
};

View file

@ -16,7 +16,8 @@ export const ResourceDetailsRoute: AppRouteObject = {
element: <ResourceDetails />,
breadcrumb: (t) => t("resourceDetails"),
handle: {
access: "view-clients",
access: (accessChecker) =>
accessChecker.hasAny("manage-clients", "view-authorization"),
},
};

View file

@ -16,7 +16,8 @@ export const ScopeDetailsRoute: AppRouteObject = {
element: <ScopeDetails />,
breadcrumb: (t) => t("authorizationScopeDetails"),
handle: {
access: "manage-clients",
access: (accessChecker) =>
accessChecker.hasAny("manage-clients", "view-authorization"),
},
};

View file

@ -22,6 +22,7 @@ import {
import { Controller } from "react-hook-form";
import { useAccess } from "../../context/access/Access";
import { FixedButtonsGroup } from "./FixedButtonGroup";
export type FormAccessProps = FormProps & {
/**
@ -89,11 +90,17 @@ export const FormAccess = ({
element.props.children,
newProps,
);
if (child.type === TextArea) {
return cloneElement(child, {
readOnly: newProps.isDisabled,
children,
} as any);
switch (child.type) {
case FixedButtonsGroup:
return cloneElement(child, {
isActive: !newProps.isDisabled,
children,
} as any);
case TextArea:
return cloneElement(child, {
readOnly: newProps.isDisabled,
children,
} as any);
}
return cloneElement(

View file

@ -32,11 +32,13 @@ export type DefaultValue = {
type KeyValueInputProps = {
name: string;
defaultKeyValue?: DefaultValue[];
isDisabled?: boolean;
};
export const KeyValueInput = ({
name,
defaultKeyValue,
isDisabled = false,
}: KeyValueInputProps) => {
const { t } = useTranslation();
const {
@ -89,6 +91,7 @@ export const KeyValueInput = ({
{...register(`${name}.${index}.key`, { required: true })}
validated={keyError ? "error" : "default"}
isRequired
isDisabled={isDisabled}
/>
)}
{keyError && (
@ -115,6 +118,7 @@ export const KeyValueInput = ({
{...register(`${name}.${index}.value`, { required: true })}
validated={valueError ? "error" : "default"}
isRequired
isDisabled={isDisabled}
/>
)}
{valueError && (
@ -131,6 +135,7 @@ export const KeyValueInput = ({
title={t("removeAttribute")}
onClick={() => remove(index)}
data-testid={`${name}-remove`}
isDisabled={isDisabled}
>
<MinusCircleIcon />
</Button>
@ -147,6 +152,7 @@ export const KeyValueInput = ({
variant="link"
icon={<PlusCircleIcon />}
onClick={appendNew}
isDisabled={isDisabled}
>
{t("addAttribute")}
</Button>
@ -166,6 +172,7 @@ export const KeyValueInput = ({
icon={<PlusCircleIcon />}
isSmall
onClick={appendNew}
isDisabled={isDisabled}
>
{t("addAttribute")}
</Button>

View file

@ -26,6 +26,7 @@ export type ListEmptyStateProps = {
icon?: ComponentClass<SVGIconProps>;
isSearchVariant?: boolean;
secondaryActions?: Action[];
isDisabled?: boolean;
};
export const ListEmptyState = ({
@ -37,6 +38,7 @@ export const ListEmptyState = ({
primaryActionText,
secondaryActions,
icon,
isDisabled = false,
}: ListEmptyStateProps) => {
return (
<EmptyState data-testid="empty-state" variant="large">
@ -56,6 +58,7 @@ export const ListEmptyState = ({
.toLowerCase()}-empty-action`}
variant="primary"
onClick={onPrimaryAction}
isDisabled={isDisabled}
>
{primaryActionText}
</Button>
@ -70,6 +73,7 @@ export const ListEmptyState = ({
.toLowerCase()}-empty-action`}
variant={action.type || ButtonVariant.secondary}
onClick={action.onClick}
isDisabled={isDisabled}
>
{action.text}
</Button>

View file

@ -27,12 +27,24 @@ export const AccessContextProvider = ({ children }: PropsWithChildren) => {
}
}, [whoAmI, realm]);
const hasAccess = (...types: AccessType[]) => {
return types.every((type) => type === "anyone" || access.includes(type));
const hasAccess = (...types: AccessType[]): boolean => {
return types.every(
(type) =>
type === "anyone" ||
(typeof type === "function" &&
type({ hasAll: hasAccess, hasAny: hasSomeAccess })) ||
access.includes(type),
);
};
const hasSomeAccess = (...types: AccessType[]) => {
return types.some((type) => type === "anyone" || access.includes(type));
const hasSomeAccess = (...types: AccessType[]): boolean => {
return types.some(
(type) =>
type === "anyone" ||
(typeof type === "function" &&
type({ hasAll: hasAccess, hasAny: hasSomeAccess })) ||
access.includes(type),
);
};
return (

View file

@ -1,3 +1,8 @@
export type AccessChecker = {
hasAll: (...types: AccessType[]) => boolean;
hasAny: (...types: AccessType[]) => boolean;
};
export type AccessTypeFunc = (accessChecker: AccessChecker) => boolean;
export type AccessType =
| "view-realm"
| "view-identity-providers"
@ -17,7 +22,8 @@ export type AccessType =
| "manage-authorization"
| "manage-clients"
| "query-groups"
| "anyone";
| "anyone"
| AccessTypeFunc;
export default interface WhoAmIRepresentation {
userId: string;