diff --git a/js/apps/admin-ui/cypress/e2e/client_authorization_test.spec.ts b/js/apps/admin-ui/cypress/e2e/client_authorization_test.spec.ts index a084d9ac10..abb28fba5e 100644 --- a/js/apps/admin-ui/cypress/e2e/client_authorization_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/client_authorization_test.spec.ts @@ -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(); diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/ListingPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/ListingPage.ts index 82f5728149..7e001eab74 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/ListingPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/ListingPage.ts @@ -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"); } diff --git a/js/apps/admin-ui/cypress/support/util/AdminClient.ts b/js/apps/admin-ui/cypress/support/util/AdminClient.ts index 43c7e36773..c6359b2e83 100644 --- a/js/apps/admin-ui/cypress/support/util/AdminClient.ts +++ b/js/apps/admin-ui/cypress/support/util/AdminClient.ts @@ -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 }); diff --git a/js/apps/admin-ui/src/ForbiddenSection.tsx b/js/apps/admin-ui/src/ForbiddenSection.tsx index 0b55ac9db7..4ccc63c4ac 100644 --- a/js/apps/admin-ui/src/ForbiddenSection.tsx +++ b/js/apps/admin-ui/src/ForbiddenSection.tsx @@ -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 ( - {t("forbidden", { count })} {permissionNeeded} + {t("forbidden", { count: permissionNeededArray.length })}{" "} + {permissionNeededArray.map((p) => p.toString())} ); }; diff --git a/js/apps/admin-ui/src/clients/ClientDetails.tsx b/js/apps/admin-ui/src/clients/ClientDetails.tsx index f92f71de82..24c1fcf801 100644 --- a/js/apps/admin-ui/src/clients/ClientDetails.tsx +++ b/js/apps/admin-ui/src/clients/ClientDetails.tsx @@ -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() { )} - {client!.authorizationServicesEnabled && hasManageAuthorization && ( - {t("authorization")}} - {...authorizationTab} - > - {t("authorization")}} + {...authorizationTab} > - {t("settings")}} - {...authorizationSettingsTab} + - - - {t("resources")}} - {...authorizationResourcesTab} - > - - - {t("scopes")}} - {...authorizationScopesTab} - > - - - {t("policies")}} - {...authorizationPoliciesTab} - > - - - {t("permissions")}} - {...authorizationPermissionsTab} - > - - - {hasViewUsers && ( {t("evaluate")}} - {...authorizationEvaluateTab} + id="settings" + data-testid="authorizationSettings" + title={{t("settings")}} + {...authorizationSettingsTab} > - + - )} - {t("export")}} - {...authorizationExportTab} - > - - - - - )} + {t("resources")}} + {...authorizationResourcesTab} + > + + + {t("scopes")}} + {...authorizationScopesTab} + > + + + {t("policies")}} + {...authorizationPoliciesTab} + > + + + {t("permissions")}} + {...authorizationPermissionsTab} + > + + + {hasViewUsers && ( + {t("evaluate")}} + {...authorizationEvaluateTab} + > + + + )} + {hasAccess("manage-authorization") && ( + {t("export")}} + {...authorizationExportTab} + > + + + )} + + + )} {client!.serviceAccountsEnabled && hasViewUsers && ( { 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}`)} diff --git a/js/apps/admin-ui/src/clients/authorization/PermissionDetails.tsx b/js/apps/admin-ui/src/clients/authorization/PermissionDetails.tsx index 81eef52220..9779db1b82 100644 --- a/js/apps/admin-ui/src/clients/authorization/PermissionDetails.tsx +++ b/js/apps/admin-ui/src/clients/authorization/PermissionDetails.tsx @@ -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(); const [applyToResourceTypeFlag, setApplyToResourceTypeFlag] = useState(false); + const { hasAccess } = useAccess(); + + const isDisabled = !hasAccess("manage-authorization"); useFetch( async () => { @@ -192,6 +196,7 @@ export default function PermissionDetails() { toggleDeleteDialog()} > {t("delete")} @@ -203,7 +208,7 @@ export default function PermissionDetails() { @@ -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}`)} diff --git a/js/apps/admin-ui/src/clients/authorization/Permissions.tsx b/js/apps/admin-ui/src/clients/authorization/Permissions.tsx index d52cef3451..e4688c674d 100644 --- a/js/apps/admin-ui/src/clients/authorization/Permissions.tsx +++ b/js/apps/admin-ui/src/clients/authorization/Permissions.tsx @@ -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={ @@ -215,7 +220,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => { navigate( @@ -233,7 +238,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => { 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) => { - - { - setSelectedResource(resource); - setPermission( - await fetchPermissions(resource._id!), - ); - toggleDeleteDialog(); - }, - }, - ], - }} - /> + {!isDisabled && ( + <> + + + + { + 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;