Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## 1.9.4 (unreleased)


- Nothing changed yet.
- Use a lightweight endpoint for realm_exists method
[remdub]


## 1.9.3 (2026-07-09)
Expand Down
34 changes: 30 additions & 4 deletions src/pas/plugins/kimug/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,17 +634,43 @@ def enable_authentication_plugins() -> None:


def realm_exists(realm: str) -> bool:
"""Check if a Keycloak realm exists."""
"""Check that a Keycloak realm exists and the admin token can manage it.

Uses the public, unauthenticated HEAD on /realms/{realm} for existence
(tiny cached endpoint, no RealmRepresentation rebuild) followed by a GET on
/admin/realms/{realm}/users/count to confirm effective admin access with a
minimal integer payload.
Comment on lines +640 to +642

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file first, then inspect the relevant sections.
wc -l src/pas/plugins/kimug/utils.py
ast-grep outline src/pas/plugins/kimug/utils.py --view expanded

sed -n '600,700p' src/pas/plugins/kimug/utils.py
rg -n "users/count|view-users|manage-users|realm-management|can count users|count users" src/pas/plugins/kimug/utils.py

Repository: IMIO/pas.plugins.kimug

Length of output: 7131


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect callers and nearby logic for realm_exists and admin operations.
rg -n "realm_exists\(" -n src/pas/plugins/kimug/utils.py src -g '*.py'
sed -n '430,520p' src/pas/plugins/kimug/utils.py
sed -n '700,740p' src/pas/plugins/kimug/utils.py

Repository: IMIO/pas.plugins.kimug

Length of output: 5541


🌐 Web query:

Keycloak users/count endpoint permissions view-users manage-users realm-management docs

💡 Result:

To access the Keycloak users/count endpoint via the Admin REST API, a user must possess sufficient permissions granted through the realm-management client [1]. Permission Requirements: The count operation on the Users resource requires permissions equivalent to querying or viewing users [2]. Specifically, the evaluator for this resource requires that the caller has at least one of the following administrative roles, which are defined within the realm-management client [2][1]: 1. view-users: Allows an administrator to view all users and groups in the realm [3][4]. 2. manage-users: Provides broader access, including all privileges granted by view-users, as well as the ability to manage user memberships, roles, and groups [3][4]. 3. query-users: A more specific role that provides permission to search and query user information [4]. Technical Implementation: The UsersResource API class handles these requests, and the underlying logic (UserPermissionEvaluator) verifies that the caller has at least one of these roles [2][5][6]. If the feature is enabled, fine-grained authorization (FGAP) may also grant access if specific permissions are configured for the user resource [3][2]. In standard configurations, possessing the view-users or manage-users role is sufficient to successfully call the count endpoint [2][5]. If a user lacks these roles, the API will return a 403 Forbidden status [7]. Ensure that the service account or user account being used to authenticate the API call is mapped to at least one of these roles under the realm-management client for the respective realm [4][1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the Keycloak user access path that follows realm_exists.
sed -n '220,360p' src/pas/plugins/kimug/utils.py
sed -n '360,470p' src/pas/plugins/kimug/utils.py
sed -n '470,620p' src/pas/plugins/kimug/utils.py

Repository: IMIO/pas.plugins.kimug

Length of output: 17141


Use a check that matches the migration’s write path. /users/count can return 200 with only view/query-user privileges, but this flow later creates and updates users. Probe an endpoint that requires the same admin rights as the migration, or rename this method so it only promises user-count access.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pas/plugins/kimug/utils.py` around lines 640 - 642, Update the
admin-access probe described near the users migration flow to use an endpoint
requiring the same create/update-user privileges as the migration’s write path,
rather than relying on /users/count; alternatively, rename the method and adjust
its contract to explicitly promise only user-count access if that is the
intended behavior.

"""
keycloak_url = os.environ.get("keycloak_url")

# HEAD on the public realm endpoint: existence check, no auth, cached,
# minimal payload (no admin RealmRepresentation involved).
realm_url = f"{keycloak_url}realms/{realm}"
head_response = requests.head(url=realm_url, timeout=10, allow_redirects=True)
if head_response.status_code != 200:
logger.error(
f"Realm '{realm}' does not exist: HEAD {realm_url} returned "
f"HTTP {head_response.status_code}"
)
return False

access_token = _get_keycloak_admin_token()
if not access_token:
logger.error("Could not get access token from Keycloak")
return False

url = f"{keycloak_url}admin/realms/{realm}"
# users/count: confirm effective admin access with a minimal payload.
headers = {"Authorization": "Bearer " + access_token}
response = requests.get(url=url, headers=headers, timeout=10)
return response.status_code == 200
count_url = f"{keycloak_url}admin/realms/{realm}/users/count"
count_response = requests.get(url=count_url, headers=headers, timeout=10)
if count_response.status_code != 200:
logger.error(
f"No admin access to realm '{realm}': GET {count_url} returned "
f"HTTP {count_response.status_code}"
)
return False

return True


def _check_redirect_uris(client_id: str, access_token: str) -> bool:
Expand Down
Loading