navigate(
@@ -366,8 +371,8 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
{noData && !searching && (
)}
{noData && searching && (
diff --git a/js/apps/admin-ui/src/clients/authorization/Policies.tsx b/js/apps/admin-ui/src/clients/authorization/Policies.tsx
index 8900709b0f..43fc496021 100644
--- a/js/apps/admin-ui/src/clients/authorization/Policies.tsx
+++ b/js/apps/admin-ui/src/clients/authorization/Policies.tsx
@@ -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) => {
/>
-
@@ -254,26 +262,28 @@ export const AuthorizationPolicies = ({ clientId }: PoliciesProps) => {
{policy.description} |
- {
- setSelectedPolicy(policy);
- toggleDeleteDialog();
+ {!isDisabled && (
+ | {
+ setSelectedPolicy(policy);
+ toggleDeleteDialog();
+ },
},
- },
- ],
- }}
- />
+ ],
+ }}
+ />
+ )}
|
|
-
+ |
{policy.isExpanded && (
{
{noData && searching && (
@@ -330,6 +341,7 @@ export const AuthorizationPolicies = ({ clientId }: PoliciesProps) => {
diff --git a/js/apps/admin-ui/src/clients/authorization/ResourceDetails.tsx b/js/apps/admin-ui/src/clients/authorization/ResourceDetails.tsx
index 7b6355a63b..d59660dabe 100644
--- a/js/apps/admin-ui/src/clients/authorization/ResourceDetails.tsx
+++ b/js/apps/admin-ui/src/clients/authorization/ResourceDetails.tsx
@@ -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() {
toggleDeleteDialog()}
>
{t("delete")}
@@ -186,7 +192,7 @@ export default function ResourceDetails() {
@@ -316,7 +322,7 @@ export default function ResourceDetails() {
}
fieldId="resourceAttribute"
>
-
+
diff --git a/js/apps/admin-ui/src/clients/authorization/Resources.tsx b/js/apps/admin-ui/src/clients/authorization/Resources.tsx
index 082eb00148..7c2cb8b2b0 100644
--- a/js/apps/admin-ui/src/clients/authorization/Resources.tsx
+++ b/js/apps/admin-ui/src/clients/authorization/Resources.tsx
@@ -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) => {
(
{
{t("type")} |
{t("owner")} |
{t("uris")} |
- |
- |
+ {!isDisabled && (
+ <>
+ |
+ |
+ >
+ )}
|
{resources.map((resource, rowIndex) => (
@@ -232,40 +241,44 @@ export const AuthorizationResources = ({ clientId }: ResourcesProps) => {
|
-
- (
-
- )}
- >
- {t("createPermission")}
-
- |
- {
- setSelectedResource(resource);
- setPermission(
- await fetchPermissions(resource._id!),
- );
- toggleDeleteDialog();
- },
- },
- ],
- }}
- />
+ {!isDisabled && (
+ <>
+ |
+ (
+
+ )}
+ >
+ {t("createPermission")}
+
+ |
+ {
+ setSelectedResource(resource);
+ setPermission(
+ await fetchPermissions(resource._id!),
+ );
+ toggleDeleteDialog();
+ },
+ },
+ ],
+ }}
+ />
+ >
+ )}
| {
navigate(toCreateResource({ realm, id: clientId }))
diff --git a/js/apps/admin-ui/src/clients/authorization/ScopeDetails.tsx b/js/apps/admin-ui/src/clients/authorization/ScopeDetails.tsx
index 72a0b98c43..3e57117959 100644
--- a/js/apps/admin-ui/src/clients/authorization/ScopeDetails.tsx
+++ b/js/apps/admin-ui/src/clients/authorization/ScopeDetails.tsx
@@ -125,7 +125,7 @@ export default function ScopeDetails() {
{
+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) => {
navigate(toNewScope({ id: clientId, realm }))}
primaryActionText={t("createAuthorizationScope")}
/>
@@ -312,6 +317,7 @@ export const AuthorizationScopes = ({ clientId }: ScopesProps) => {
{noData && searching && (
diff --git a/js/apps/admin-ui/src/clients/authorization/Settings.tsx b/js/apps/admin-ui/src/clients/authorization/Settings.tsx
index 79c9dedefc..411c3b8116 100644
--- a/js/apps/admin-ui/src/clients/authorization/Settings.tsx
+++ b/js/apps/admin-ui/src/clients/authorization/Settings.tsx
@@ -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 }) => {
/>
)}
@@ -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}`)}
diff --git a/js/apps/admin-ui/src/clients/routes/AuthenticationTab.tsx b/js/apps/admin-ui/src/clients/routes/AuthenticationTab.tsx
index b145c39703..8ee4dcd39a 100644
--- a/js/apps/admin-ui/src/clients/routes/AuthenticationTab.tsx
+++ b/js/apps/admin-ui/src/clients/routes/AuthenticationTab.tsx
@@ -25,7 +25,8 @@ export const AuthorizationRoute: AppRouteObject = {
element: ,
breadcrumb: (t) => t("clientSettings"),
handle: {
- access: "manage-authorization",
+ access: (accessChecker) =>
+ accessChecker.hasAny("view-authorization", "manage-authorization"),
},
};
diff --git a/js/apps/admin-ui/src/clients/routes/NewPermission.tsx b/js/apps/admin-ui/src/clients/routes/NewPermission.tsx
index c65dd01069..4cfe546e1c 100644
--- a/js/apps/admin-ui/src/clients/routes/NewPermission.tsx
+++ b/js/apps/admin-ui/src/clients/routes/NewPermission.tsx
@@ -21,7 +21,8 @@ export const NewPermissionRoute: AppRouteObject = {
element: ,
breadcrumb: (t) => t("createPermission"),
handle: {
- access: "view-clients",
+ access: (accessChecker) =>
+ accessChecker.hasAny("manage-clients", "manage-authorization"),
},
};
diff --git a/js/apps/admin-ui/src/clients/routes/NewPolicy.tsx b/js/apps/admin-ui/src/clients/routes/NewPolicy.tsx
index 5afea2aa30..64f9d86cc5 100644
--- a/js/apps/admin-ui/src/clients/routes/NewPolicy.tsx
+++ b/js/apps/admin-ui/src/clients/routes/NewPolicy.tsx
@@ -14,7 +14,8 @@ export const NewPolicyRoute: AppRouteObject = {
element: ,
breadcrumb: (t) => t("createPolicy"),
handle: {
- access: "view-clients",
+ access: (accessChecker) =>
+ accessChecker.hasAny("manage-clients", "manage-authorization"),
},
};
diff --git a/js/apps/admin-ui/src/clients/routes/NewResource.tsx b/js/apps/admin-ui/src/clients/routes/NewResource.tsx
index 4acf211259..465e47308c 100644
--- a/js/apps/admin-ui/src/clients/routes/NewResource.tsx
+++ b/js/apps/admin-ui/src/clients/routes/NewResource.tsx
@@ -12,7 +12,8 @@ export const NewResourceRoute: AppRouteObject = {
element: ,
breadcrumb: (t) => t("createResource"),
handle: {
- access: "view-clients",
+ access: (accessChecker) =>
+ accessChecker.hasAny("manage-clients", "manage-authorization"),
},
};
diff --git a/js/apps/admin-ui/src/clients/routes/NewScope.tsx b/js/apps/admin-ui/src/clients/routes/NewScope.tsx
index 488a3bf9b3..e40ffdd8e7 100644
--- a/js/apps/admin-ui/src/clients/routes/NewScope.tsx
+++ b/js/apps/admin-ui/src/clients/routes/NewScope.tsx
@@ -12,7 +12,8 @@ export const NewScopeRoute: AppRouteObject = {
element: ,
breadcrumb: (t) => t("createAuthorizationScope"),
handle: {
- access: "view-clients",
+ access: (accessChecker) =>
+ accessChecker.hasAny("manage-clients", "manage-authorization"),
},
};
diff --git a/js/apps/admin-ui/src/clients/routes/PermissionDetails.tsx b/js/apps/admin-ui/src/clients/routes/PermissionDetails.tsx
index d3673302e1..a978f023e6 100644
--- a/js/apps/admin-ui/src/clients/routes/PermissionDetails.tsx
+++ b/js/apps/admin-ui/src/clients/routes/PermissionDetails.tsx
@@ -20,7 +20,8 @@ export const PermissionDetailsRoute: AppRouteObject = {
element: ,
breadcrumb: (t) => t("permissionDetails"),
handle: {
- access: "view-clients",
+ access: (accessChecker) =>
+ accessChecker.hasAny("manage-clients", "view-authorization"),
},
};
diff --git a/js/apps/admin-ui/src/clients/routes/PolicyDetails.tsx b/js/apps/admin-ui/src/clients/routes/PolicyDetails.tsx
index 5332ce5641..1c1a5b44d7 100644
--- a/js/apps/admin-ui/src/clients/routes/PolicyDetails.tsx
+++ b/js/apps/admin-ui/src/clients/routes/PolicyDetails.tsx
@@ -19,7 +19,8 @@ export const PolicyDetailsRoute: AppRouteObject = {
element: ,
breadcrumb: (t) => t("policyDetails"),
handle: {
- access: "view-clients",
+ access: (accessChecker) =>
+ accessChecker.hasAny("manage-clients", "view-authorization"),
},
};
diff --git a/js/apps/admin-ui/src/clients/routes/Resource.tsx b/js/apps/admin-ui/src/clients/routes/Resource.tsx
index 661e1da1df..3cd567c448 100644
--- a/js/apps/admin-ui/src/clients/routes/Resource.tsx
+++ b/js/apps/admin-ui/src/clients/routes/Resource.tsx
@@ -16,7 +16,8 @@ export const ResourceDetailsRoute: AppRouteObject = {
element: ,
breadcrumb: (t) => t("resourceDetails"),
handle: {
- access: "view-clients",
+ access: (accessChecker) =>
+ accessChecker.hasAny("manage-clients", "view-authorization"),
},
};
diff --git a/js/apps/admin-ui/src/clients/routes/Scope.tsx b/js/apps/admin-ui/src/clients/routes/Scope.tsx
index 68d1e12226..a228a005e8 100644
--- a/js/apps/admin-ui/src/clients/routes/Scope.tsx
+++ b/js/apps/admin-ui/src/clients/routes/Scope.tsx
@@ -16,7 +16,8 @@ export const ScopeDetailsRoute: AppRouteObject = {
element: ,
breadcrumb: (t) => t("authorizationScopeDetails"),
handle: {
- access: "manage-clients",
+ access: (accessChecker) =>
+ accessChecker.hasAny("manage-clients", "view-authorization"),
},
};
diff --git a/js/apps/admin-ui/src/components/form/FormAccess.tsx b/js/apps/admin-ui/src/components/form/FormAccess.tsx
index 2cf5b2c61f..a61f016b40 100644
--- a/js/apps/admin-ui/src/components/form/FormAccess.tsx
+++ b/js/apps/admin-ui/src/components/form/FormAccess.tsx
@@ -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(
diff --git a/js/apps/admin-ui/src/components/key-value-form/KeyValueInput.tsx b/js/apps/admin-ui/src/components/key-value-form/KeyValueInput.tsx
index 94dfdd42da..96adf40ed3 100644
--- a/js/apps/admin-ui/src/components/key-value-form/KeyValueInput.tsx
+++ b/js/apps/admin-ui/src/components/key-value-form/KeyValueInput.tsx
@@ -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}
>
@@ -147,6 +152,7 @@ export const KeyValueInput = ({
variant="link"
icon={}
onClick={appendNew}
+ isDisabled={isDisabled}
>
{t("addAttribute")}
@@ -166,6 +172,7 @@ export const KeyValueInput = ({
icon={}
isSmall
onClick={appendNew}
+ isDisabled={isDisabled}
>
{t("addAttribute")}
diff --git a/js/apps/admin-ui/src/components/list-empty-state/ListEmptyState.tsx b/js/apps/admin-ui/src/components/list-empty-state/ListEmptyState.tsx
index 5df99bbc99..3b400d1967 100644
--- a/js/apps/admin-ui/src/components/list-empty-state/ListEmptyState.tsx
+++ b/js/apps/admin-ui/src/components/list-empty-state/ListEmptyState.tsx
@@ -26,6 +26,7 @@ export type ListEmptyStateProps = {
icon?: ComponentClass;
isSearchVariant?: boolean;
secondaryActions?: Action[];
+ isDisabled?: boolean;
};
export const ListEmptyState = ({
@@ -37,6 +38,7 @@ export const ListEmptyState = ({
primaryActionText,
secondaryActions,
icon,
+ isDisabled = false,
}: ListEmptyStateProps) => {
return (
@@ -56,6 +58,7 @@ export const ListEmptyState = ({
.toLowerCase()}-empty-action`}
variant="primary"
onClick={onPrimaryAction}
+ isDisabled={isDisabled}
>
{primaryActionText}
@@ -70,6 +73,7 @@ export const ListEmptyState = ({
.toLowerCase()}-empty-action`}
variant={action.type || ButtonVariant.secondary}
onClick={action.onClick}
+ isDisabled={isDisabled}
>
{action.text}
diff --git a/js/apps/admin-ui/src/context/access/Access.tsx b/js/apps/admin-ui/src/context/access/Access.tsx
index 81a3d0f39b..6ab2f740fa 100644
--- a/js/apps/admin-ui/src/context/access/Access.tsx
+++ b/js/apps/admin-ui/src/context/access/Access.tsx
@@ -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 (
diff --git a/js/libs/keycloak-admin-client/src/defs/whoAmIRepresentation.ts b/js/libs/keycloak-admin-client/src/defs/whoAmIRepresentation.ts
index 1086f93a82..e2560e8d1f 100644
--- a/js/libs/keycloak-admin-client/src/defs/whoAmIRepresentation.ts
+++ b/js/libs/keycloak-admin-client/src/defs/whoAmIRepresentation.ts
@@ -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;