Skip to content

Commit bacd860

Browse files
timbl-ontclaude
andcommitted
Address SonarCloud maintainability findings
- oauth_validator: use a dict literal instead of the dict() constructor (python:S7498). - Extract the OAuth scope check from tenant_authentication into _enforce_tenant_scope to reduce cognitive complexity (python:S3776). - Extract OAuth validation and settings mapping from AdminGroup.get_settings into _validate_admin_auth_args and _oauth_settings helpers, reducing its cognitive complexity and collapsing the nested if (S3776, S1066). - Redirect demo-script ERROR messages to stderr (shell S7677). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Tim Bloomfield <tim.bloomfield@ontario.ca>
1 parent 60f2843 commit bacd860

6 files changed

Lines changed: 114 additions & 93 deletions

File tree

acapy_agent/admin/decorators/auth.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -74,23 +74,9 @@ async def tenant_auth(request):
7474
return await handler(request)
7575

7676
# OAuth path: token was validated by setup_context middleware.
77-
# acapy:tenant or acapy:admin both grant tenant-level access.
78-
# acapy:tenant:read grants read-only access (safe HTTP methods only).
7977
if has_auth_scopes(context):
80-
scopes = get_auth_scopes(context)
81-
if scopes & {"acapy:tenant", "acapy:admin"}:
82-
return await handler(request)
83-
if "acapy:tenant:read" in scopes:
84-
if request.method in ("GET", "HEAD"):
85-
return await handler(request)
86-
raise web.HTTPForbidden(
87-
reason="acapy:tenant:read scope does not permit write operations",
88-
text="acapy:tenant:read scope does not permit write operations",
89-
)
90-
raise web.HTTPForbidden(
91-
reason="acapy:tenant scope required",
92-
text="acapy:tenant scope required",
93-
)
78+
_enforce_tenant_scope(get_auth_scopes(context), request.method)
79+
return await handler(request)
9480

9581
authorization_header = request.headers.get("Authorization")
9682
header_admin_api_key = request.headers.get("x-api-key")
@@ -162,6 +148,27 @@ async def scope_check(request):
162148
return decorator
163149

164150

151+
def _enforce_tenant_scope(scopes: set, method: str) -> None:
152+
"""Authorize a tenant request by its OAuth scopes, else raise HTTPForbidden.
153+
154+
``acapy:tenant`` or ``acapy:admin`` grant tenant-level access;
155+
``acapy:tenant:read`` grants read-only access (safe HTTP methods only).
156+
"""
157+
if scopes & {"acapy:tenant", "acapy:admin"}:
158+
return
159+
if "acapy:tenant:read" in scopes:
160+
if method in ("GET", "HEAD"):
161+
return
162+
raise web.HTTPForbidden(
163+
reason="acapy:tenant:read scope does not permit write operations",
164+
text="acapy:tenant:read scope does not permit write operations",
165+
)
166+
raise web.HTTPForbidden(
167+
reason="acapy:tenant scope required",
168+
text="acapy:tenant scope required",
169+
)
170+
171+
165172
def _base_wallet_route_access(additional_routes: List[str], request_path: str) -> bool:
166173
"""Check if request path matches additional routes."""
167174
additional_routes_pattern = (

acapy_agent/admin/oauth_validator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,15 @@ async def _validate_jwt(self, token: str) -> dict:
121121
None, self._jwks_client.get_signing_key_from_jwt, token
122122
)
123123

124-
decode_kwargs = dict(
125-
algorithms=_SUPPORTED_ALGORITHMS,
124+
decode_kwargs = {
125+
"algorithms": _SUPPORTED_ALGORITHMS,
126126
# Without verify_aud=False, PyJWT rejects any token carrying an
127127
# aud claim when no expected audience is configured.
128-
options={
128+
"options": {
129129
"require": ["exp", "iss", "sub"],
130130
"verify_aud": self.audience is not None,
131131
},
132-
)
132+
}
133133
if self.issuer:
134134
decode_kwargs["issuer"] = self.issuer
135135
if self.audience:

acapy_agent/config/argparse.py

Lines changed: 74 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,76 @@ def add_arguments(self, parser: ArgumentParser):
379379
),
380380
)
381381

382+
@staticmethod
383+
def _validate_admin_auth_args(
384+
args: Namespace, oauth_mode: bool, admin_api_key, admin_insecure_mode
385+
):
386+
"""Validate admin authentication argument combinations.
387+
388+
Raises:
389+
ArgsParseError: if the admin auth flags are inconsistent.
390+
391+
"""
392+
if not oauth_mode:
393+
if (admin_api_key and admin_insecure_mode) or not (
394+
admin_api_key or admin_insecure_mode
395+
):
396+
raise ArgsParseError(
397+
"Either --admin-api-key or --admin-insecure-mode "
398+
"must be set but not both, unless --oauth-enabled (or "
399+
"--oauth-jwks-uri / --oauth-introspection-endpoint) "
400+
"is configured."
401+
)
402+
return
403+
404+
if not (
405+
getattr(args, "oauth_jwks_uri", None)
406+
or getattr(args, "oauth_introspection_endpoint", None)
407+
):
408+
raise ArgsParseError(
409+
"OAuth mode requires a token validation method: set "
410+
"--oauth-jwks-uri and/or --oauth-introspection-endpoint."
411+
)
412+
if getattr(args, "oauth_introspection_endpoint", None) and not getattr(
413+
args, "oauth_introspection_client_id", None
414+
):
415+
raise ArgsParseError(
416+
"--oauth-introspection-endpoint requires --oauth-introspection-client-id."
417+
)
418+
if getattr(args, "oauth_jwks_uri", None) and not getattr(
419+
args, "oauth_audience", None
420+
):
421+
# Without an expected audience, any signature-valid token from the
422+
# JWKS is accepted regardless of its intended recipient, allowing
423+
# token reuse / confused-deputy on a shared AS.
424+
raise ArgsParseError(
425+
"--oauth-jwks-uri requires --oauth-audience so that JWT "
426+
"access tokens are bound to this resource server (the "
427+
"'aud' claim is verified)."
428+
)
429+
430+
@staticmethod
431+
def _oauth_settings(args: Namespace, oauth_mode: bool) -> dict:
432+
"""Build the oauth.* settings map from parsed arguments."""
433+
settings = {}
434+
if oauth_mode:
435+
settings["admin.oauth_enabled"] = True
436+
settings["oauth.http_timeout"] = getattr(args, "oauth_http_timeout", None)
437+
438+
arg_to_setting = {
439+
"oauth_jwks_uri": "oauth.jwks_uri",
440+
"oauth_issuer": "oauth.issuer",
441+
"oauth_audience": "oauth.audience",
442+
"oauth_introspection_endpoint": "oauth.introspection_endpoint",
443+
"oauth_introspection_client_id": "oauth.introspection_client_id",
444+
"oauth_introspection_client_secret": "oauth.introspection_client_secret",
445+
}
446+
for arg_name, setting_key in arg_to_setting.items():
447+
value = getattr(args, arg_name, None)
448+
if value:
449+
settings[setting_key] = value
450+
return settings
451+
382452
def get_settings(self, args: Namespace):
383453
"""Extract admin settings."""
384454
settings = {}
@@ -391,69 +461,13 @@ def get_settings(self, args: Namespace):
391461
or getattr(args, "oauth_introspection_endpoint", None)
392462
)
393463

394-
if not oauth_mode:
395-
if (admin_api_key and admin_insecure_mode) or not (
396-
admin_api_key or admin_insecure_mode
397-
):
398-
raise ArgsParseError(
399-
"Either --admin-api-key or --admin-insecure-mode "
400-
"must be set but not both, unless --oauth-enabled (or "
401-
"--oauth-jwks-uri / --oauth-introspection-endpoint) "
402-
"is configured."
403-
)
404-
else:
405-
if not (
406-
getattr(args, "oauth_jwks_uri", None)
407-
or getattr(args, "oauth_introspection_endpoint", None)
408-
):
409-
raise ArgsParseError(
410-
"OAuth mode requires a token validation method: set "
411-
"--oauth-jwks-uri and/or --oauth-introspection-endpoint."
412-
)
413-
if getattr(args, "oauth_introspection_endpoint", None) and not getattr(
414-
args, "oauth_introspection_client_id", None
415-
):
416-
raise ArgsParseError(
417-
"--oauth-introspection-endpoint requires "
418-
"--oauth-introspection-client-id."
419-
)
420-
if getattr(args, "oauth_jwks_uri", None) and not getattr(
421-
args, "oauth_audience", None
422-
):
423-
# Without an expected audience, any signature-valid token from
424-
# the JWKS is accepted regardless of its intended recipient,
425-
# allowing token reuse / confused-deputy on a shared AS.
426-
raise ArgsParseError(
427-
"--oauth-jwks-uri requires --oauth-audience so that JWT "
428-
"access tokens are bound to this resource server (the "
429-
"'aud' claim is verified)."
430-
)
464+
self._validate_admin_auth_args(
465+
args, oauth_mode, admin_api_key, admin_insecure_mode
466+
)
431467

432468
settings["admin.admin_api_key"] = admin_api_key
433469
settings["admin.admin_insecure_mode"] = admin_insecure_mode
434-
435-
if oauth_mode:
436-
settings["admin.oauth_enabled"] = True
437-
settings["oauth.http_timeout"] = getattr(args, "oauth_http_timeout", None)
438-
439-
if getattr(args, "oauth_jwks_uri", None):
440-
settings["oauth.jwks_uri"] = args.oauth_jwks_uri
441-
if getattr(args, "oauth_issuer", None):
442-
settings["oauth.issuer"] = args.oauth_issuer
443-
if getattr(args, "oauth_audience", None):
444-
settings["oauth.audience"] = args.oauth_audience
445-
if getattr(args, "oauth_introspection_endpoint", None):
446-
settings["oauth.introspection_endpoint"] = (
447-
args.oauth_introspection_endpoint
448-
)
449-
if getattr(args, "oauth_introspection_client_id", None):
450-
settings["oauth.introspection_client_id"] = (
451-
args.oauth_introspection_client_id
452-
)
453-
if getattr(args, "oauth_introspection_client_secret", None):
454-
settings["oauth.introspection_client_secret"] = (
455-
args.oauth_introspection_client_secret
456-
)
470+
settings.update(self._oauth_settings(args, oauth_mode))
457471

458472
settings["admin.enabled"] = True
459473
settings["admin.host"] = args.admin[0]

demo/demo-authserver/scripts/get-user-token.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ WALLET_ID=$(curl -sf \
7979
| jq -r '.[] | select(.name == "wallet-id") | .config["claim.value"] // empty')
8080

8181
if [[ -z "${WALLET_ID}" || "${WALLET_ID}" == "PLACEHOLDER_WALLET_ID" ]]; then
82-
echo "ERROR: wallet_id not set on '${TENANT_CLIENT_ID}'. Run ./scripts/setup-tenant.sh first."
82+
echo "ERROR: wallet_id not set on '${TENANT_CLIENT_ID}'. Run ./scripts/setup-tenant.sh first." >&2
8383
exit 1
8484
fi
8585

@@ -225,7 +225,7 @@ print(result[0] if result else '')
225225
")
226226

227227
if [[ -z "${AUTH_CODE}" ]]; then
228-
echo "ERROR: No authorization code received."
228+
echo "ERROR: No authorization code received." >&2
229229
exit 1
230230
fi
231231

@@ -244,8 +244,8 @@ TOKEN_RESPONSE=$(curl -s -X POST "${TOKEN_ENDPOINT}" \
244244
ACCESS_TOKEN=$(echo "${TOKEN_RESPONSE}" | jq -r '.access_token // empty')
245245

246246
if [[ -z "${ACCESS_TOKEN}" ]]; then
247-
echo "ERROR: Token exchange failed."
248-
echo " Response: ${TOKEN_RESPONSE}"
247+
echo "ERROR: Token exchange failed." >&2
248+
echo " Response: ${TOKEN_RESPONSE}" >&2
249249
exit 1
250250
fi
251251

demo/demo-authserver/scripts/setup-tenant.sh

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ if [[ -z "${WALLET_ID}" ]]; then
9797
fi
9898

9999
if [[ -z "${WALLET_ID}" ]]; then
100-
echo "ERROR: Could not create or find wallet '${WALLET_NAME}'."
100+
echo "ERROR: Could not create or find wallet '${WALLET_NAME}'." >&2
101101
exit 1
102102
fi
103103
echo " wallet_id: ${WALLET_ID}"
@@ -112,7 +112,7 @@ KC_CLIENT_UUID=$(curl -sf \
112112
| jq -r '.[0].id // empty')
113113

114114
if [[ -z "${KC_CLIENT_UUID}" ]]; then
115-
echo "ERROR: Client '${TENANT_CLIENT_ID}' not found in Keycloak realm '${KEYCLOAK_REALM}'."
115+
echo "ERROR: Client '${TENANT_CLIENT_ID}' not found in Keycloak realm '${KEYCLOAK_REALM}'." >&2
116116
exit 1
117117
fi
118118
echo " client UUID: ${KC_CLIENT_UUID}"
@@ -152,7 +152,7 @@ CREATE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
152152
}
153153
}")
154154
if [[ "${CREATE_STATUS}" != "201" ]]; then
155-
echo "ERROR: Failed to create wallet-id mapper (HTTP ${CREATE_STATUS})."
155+
echo "ERROR: Failed to create wallet-id mapper (HTTP ${CREATE_STATUS})." >&2
156156
exit 1
157157
fi
158158
echo " Mapper created (HTTP ${CREATE_STATUS})."
@@ -164,7 +164,7 @@ ACTUAL_WALLET_ID=$(curl -sf \
164164
| jq -r '.[] | select(.name == "wallet-id") | .config["claim.value"] // empty')
165165
echo " Verified claim.value in Keycloak: ${ACTUAL_WALLET_ID}"
166166
if [[ "${ACTUAL_WALLET_ID}" != "${WALLET_ID}" ]]; then
167-
echo "ERROR: Keycloak mapper value does not match wallet_id (${ACTUAL_WALLET_ID} != ${WALLET_ID})."
167+
echo "ERROR: Keycloak mapper value does not match wallet_id (${ACTUAL_WALLET_ID} != ${WALLET_ID})." >&2
168168
exit 1
169169
fi
170170

@@ -268,8 +268,8 @@ WALLET_CREATE_SCOPE_UUID=$(curl -sf \
268268
| jq -r '.[] | select(.name == "acapy:wallet:create") | .id // empty')
269269

270270
if [[ -z "${WALLET_CREATE_SCOPE_UUID}" ]]; then
271-
echo "ERROR: 'acapy:wallet:create' client scope not found in realm '${KEYCLOAK_REALM}'."
272-
echo " Ensure the realm was imported from keycloak/realm-export.json."
271+
echo "ERROR: 'acapy:wallet:create' client scope not found in realm '${KEYCLOAK_REALM}'." >&2
272+
echo " Ensure the realm was imported from keycloak/realm-export.json." >&2
273273
exit 1
274274
fi
275275
echo " acapy:wallet:create scope UUID: ${WALLET_CREATE_SCOPE_UUID}"
@@ -281,7 +281,7 @@ ASSIGN_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
281281
if [[ "${ASSIGN_STATUS}" == "204" || "${ASSIGN_STATUS}" == "409" ]]; then
282282
echo " acapy:wallet:create scope assigned (HTTP ${ASSIGN_STATUS})."
283283
else
284-
echo "ERROR: Failed to assign acapy:wallet:create scope (HTTP ${ASSIGN_STATUS})."
284+
echo "ERROR: Failed to assign acapy:wallet:create scope (HTTP ${ASSIGN_STATUS})." >&2
285285
exit 1
286286
fi
287287

demo/demo-authserver/scripts/test-oauth-scopes.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ WALLET_ID=$(echo "${TENANT_TOKEN}" \
153153

154154
if [[ -z "${WALLET_ID}" || "${WALLET_ID}" == "PLACEHOLDER_WALLET_ID" ]]; then
155155
echo ""
156-
echo "ERROR: tenant token does not contain a valid wallet_id claim."
157-
echo " Run ./scripts/setup-tenant.sh first."
156+
echo "ERROR: tenant token does not contain a valid wallet_id claim." >&2
157+
echo " Run ./scripts/setup-tenant.sh first." >&2
158158
exit 1
159159
fi
160160
echo " wallet_id: ${WALLET_ID}"

0 commit comments

Comments
 (0)