Skip to content

Commit cf61e20

Browse files
committed
Get groups from OIDC before falling back to LDAP
JIRA: RHELWF-13972 Assisted-by: Claude Code
1 parent f28eb9d commit cf61e20

4 files changed

Lines changed: 178 additions & 7 deletions

File tree

resultsdb/authorization.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,49 @@ def match_testcase_permissions(testcase, permissions):
4040
yield permission
4141

4242

43-
def verify_authorization(user, testcase, permissions, ldap_host, ldap_searches):
43+
def verify_authorization(
44+
user, testcase, permissions, ldap_host, ldap_searches, oidc_groups=None
45+
):
4446
"""
4547
Raises an exception if the user is not permitted to publish a result for
4648
the testcase.
4749
"""
48-
if not (ldap_host and ldap_searches):
49-
raise InternalServerError(
50-
"LDAP_HOST and LDAP_SEARCHES also need to be defined if PERMISSIONS is defined"
51-
)
52-
5350
allowed_groups = []
5451
for permission in match_testcase_permissions(testcase, permissions):
5552
if user in permission.get("users", []):
5653
return
5754
allowed_groups += permission.get("groups", [])
5855

56+
# OIDC groups check (no network calls needed)
57+
if oidc_groups is not None:
58+
if not oidc_groups:
59+
log.warning(
60+
"OIDC token for user %s contains an empty groups claim; "
61+
"falling back to LDAP for test case %s",
62+
user,
63+
testcase,
64+
)
65+
elif not set(oidc_groups).isdisjoint(allowed_groups):
66+
return
67+
else:
68+
log.warning(
69+
"OIDC groups %r did not match any allowed groups %r for user %s "
70+
"and test case %s; falling back to LDAP",
71+
oidc_groups,
72+
allowed_groups,
73+
user,
74+
testcase,
75+
)
76+
77+
if not (ldap_host and ldap_searches):
78+
if oidc_groups is None:
79+
raise InternalServerError(
80+
"LDAP_HOST and LDAP_SEARCHES also need to be defined if PERMISSIONS is defined"
81+
)
82+
raise Forbidden(
83+
f"User {user} is not authorized to submit results for the test case {testcase}"
84+
)
85+
5986
try:
6087
import ldap
6188
except ImportError:
@@ -73,6 +100,14 @@ def verify_authorization(user, testcase, permissions, ldap_host, ldap_searches):
73100
for cur_ldap_search in ldap_searches:
74101
groups = get_group_membership(ldap, user, con, cur_ldap_search)
75102
if any(g in groups for g in allowed_groups):
103+
if oidc_groups is not None:
104+
log.warning(
105+
"LDAP authorized user %s for test case %s. "
106+
"Consider adding the appropriate groups to the user's "
107+
"OIDC token to avoid LDAP dependency.",
108+
user,
109+
testcase,
110+
)
76111
return
77112
any_groups_found = any_groups_found or len(groups) > 0
78113

resultsdb/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class Config:
8888

8989
OIDC_CLIENT_SECRETS = "/etc/resultsdb/oauth2_client_secrets.json"
9090
OIDC_USERNAME_FIELD = "uid"
91+
OIDC_GROUPS_FIELD = "realm_access.roles"
9192
OIDC_SESSION_REFRESH_INTERVAL_SECONDS = 300
9293
OIDC_SESSION_PERMANENT = False
9394
PERMANENT_SESSION_LIFETIME = 300

resultsdb/controllers/api_v3.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,46 @@ def permissions():
2828
return app.config.get("PERMISSIONS", [])
2929

3030

31+
def get_oidc_groups():
32+
groups_field = app.config.get("OIDC_GROUPS_FIELD")
33+
if not groups_field:
34+
return None
35+
36+
token_identity = app.oidc.current_token_identity
37+
38+
value = token_identity
39+
for key in groups_field.split("."):
40+
if not isinstance(value, dict):
41+
app.logger.warning(
42+
"OIDC groups claim %r not found in token (failed at %r)",
43+
groups_field,
44+
key,
45+
)
46+
return None
47+
value = value.get(key)
48+
if value is None:
49+
app.logger.warning(
50+
"OIDC groups claim %r not found in token (missing key %r)",
51+
groups_field,
52+
key,
53+
)
54+
return None
55+
56+
return value
57+
58+
3159
def get_authorized_user(testcase) -> str:
3260
"""
3361
Raises an exception if the current user cannot publish a result for the
3462
testcase, otherwise returns the name of the current user.
3563
"""
3664
user = app.oidc.current_token_identity[app.config["OIDC_USERNAME_FIELD"]]
65+
oidc_groups = get_oidc_groups()
3766
ldap_host = app.config.get("LDAP_HOST")
3867
ldap_searches = app.config.get("LDAP_SEARCHES")
39-
verify_authorization(user, testcase, permissions(), ldap_host, ldap_searches)
68+
verify_authorization(
69+
user, testcase, permissions(), ldap_host, ldap_searches, oidc_groups
70+
)
4071
return user
4172

4273

tests/test_api_v3.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,110 @@ def test_api_v3_permission_no_groups_found(client, permissions, mock_ldap, caplo
433433
assert f"Permission denied: {expected_error}" in caplog.text
434434

435435

436+
def test_api_v3_permission_oidc_groups_match(client, permissions, mock_ldap, app):
437+
"""OIDC groups match — authorized without LDAP."""
438+
permissions.append(
439+
{
440+
"groups": ["testgroup1"],
441+
"testcases": ["testcase1*"],
442+
}
443+
)
444+
app.oidc.current_token_identity = {
445+
"uid": "testuser1",
446+
"realm_access": {"roles": ["testgroup1"]},
447+
}
448+
data = brew_build_request_data()
449+
r = client.post("/api/v3/results/brew-builds", json=data)
450+
assert r.status_code == 201, r.text
451+
mock_ldap.search_s.assert_not_called()
452+
app.oidc.current_token_identity = {"uid": "testuser1"}
453+
454+
455+
def test_api_v3_permission_oidc_groups_no_match_ldap_fallback(
456+
client, permissions, mock_ldap, caplog, app
457+
):
458+
"""OIDC groups don't match, LDAP authorizes with deprecation warning."""
459+
permissions.append(
460+
{
461+
"groups": ["testgroup1"],
462+
"testcases": ["testcase1*"],
463+
}
464+
)
465+
app.oidc.current_token_identity = {
466+
"uid": "testuser1",
467+
"realm_access": {"roles": ["other_group"]},
468+
}
469+
data = brew_build_request_data()
470+
r = client.post("/api/v3/results/brew-builds", json=data)
471+
assert r.status_code == 201, r.text
472+
mock_ldap.search_s.assert_called_once()
473+
assert "falling back to LDAP" in caplog.text
474+
assert "LDAP authorized" in caplog.text
475+
app.oidc.current_token_identity = {"uid": "testuser1"}
476+
477+
478+
def test_api_v3_permission_oidc_groups_no_match_ldap_denies(
479+
client, permissions, mock_ldap, caplog, app
480+
):
481+
"""OIDC groups don't match, LDAP doesn't authorize — Forbidden."""
482+
permissions.append(
483+
{
484+
"groups": ["testgroup1"],
485+
"testcases": ["testcase1*"],
486+
}
487+
)
488+
app.oidc.current_token_identity = {
489+
"uid": "testuser1",
490+
"realm_access": {"roles": ["other_group"]},
491+
}
492+
mock_ldap.search_s.return_value = []
493+
data = brew_build_request_data()
494+
r = client.post("/api/v3/results/brew-builds", json=data)
495+
assert r.status_code == 403, r.text
496+
assert "falling back to LDAP" in caplog.text
497+
app.oidc.current_token_identity = {"uid": "testuser1"}
498+
499+
500+
def test_api_v3_permission_oidc_groups_match_without_ldap(
501+
client, permissions, mock_ldap, app
502+
):
503+
"""OIDC groups match — authorized even when LDAP is not configured."""
504+
permissions.append(
505+
{
506+
"groups": ["testgroup1"],
507+
"testcases": ["testcase1*"],
508+
}
509+
)
510+
app.oidc.current_token_identity = {
511+
"uid": "testuser1",
512+
"realm_access": {"roles": ["testgroup1"]},
513+
}
514+
data = brew_build_request_data()
515+
with patch.dict(app.config, {"LDAP_HOST": None, "LDAP_SEARCHES": None}):
516+
r = client.post("/api/v3/results/brew-builds", json=data)
517+
assert r.status_code == 201, r.text
518+
mock_ldap.search_s.assert_not_called()
519+
app.oidc.current_token_identity = {"uid": "testuser1"}
520+
521+
522+
def test_api_v3_permission_oidc_groups_none_falls_back_to_ldap(
523+
client, permissions, mock_ldap, app
524+
):
525+
"""No OIDC groups claim — falls back to LDAP as before."""
526+
permissions.append(
527+
{
528+
"groups": ["testgroup1"],
529+
"testcases": ["testcase1*"],
530+
}
531+
)
532+
# current_token_identity has no realm_access — so get_oidc_groups returns None
533+
app.oidc.current_token_identity = {"uid": "testuser1"}
534+
data = brew_build_request_data()
535+
r = client.post("/api/v3/results/brew-builds", json=data)
536+
assert r.status_code == 201, r.text
537+
mock_ldap.search_s.assert_called_once()
538+
539+
436540
@pytest.mark.parametrize("params_class", RESULTS_PARAMS_CLASSES)
437541
def test_api_v3_consistency(params_class, client):
438542
"""

0 commit comments

Comments
 (0)