Skip to content

Commit a96c763

Browse files
authored
OIDC: default to disabled user if not in mapped group (#719)
* OIDC: default to disabled user if not in mapped group * Fix test
1 parent 913c680 commit a96c763

File tree

2 files changed

+110
-41
lines changed

2 files changed

+110
-41
lines changed

gramps_webapi/auth/oidc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,9 @@ def get_role_from_claims(
221221
logger.warning(
222222
f"No '{role_claim}' claim found in user claims. Assigning guest role."
223223
)
224-
return ROLE_GUEST
224+
return ROLE_DISABLED
225225

226-
highest_role = ROLE_GUEST
226+
highest_role = ROLE_DISABLED
227227

228228
for role_level in sorted(role_mapping.keys(), reverse=True):
229229
group_name = role_mapping[role_level]

tests/test_oidc_auth.py

Lines changed: 108 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def test_empty_groups_with_mapping(self):
6666
with patch("gramps_webapi.auth.oidc.current_app", mock_app):
6767
user_claims = {"groups": []}
6868
role = get_role_from_claims(user_claims)
69-
assert role == ROLE_GUEST
69+
assert role == ROLE_DISABLED
7070

7171
def test_no_matching_groups(self):
7272
"""Test role mapping with no matching groups."""
@@ -79,7 +79,7 @@ def test_no_matching_groups(self):
7979
with patch("gramps_webapi.auth.oidc.current_app", mock_app):
8080
user_claims = {"groups": ["unknown-group", "another-unknown"]}
8181
role = get_role_from_claims(user_claims)
82-
assert role == ROLE_GUEST
82+
assert role == ROLE_DISABLED
8383

8484
def test_single_role_match(self):
8585
"""Test role mapping with single matching group."""
@@ -120,15 +120,21 @@ def test_multiple_matches_highest_precedence_wins(self):
120120

121121
with patch("gramps_webapi.auth.oidc.current_app", mock_app):
122122
# Admin (5) should win over all others
123-
role = get_role_from_claims({"groups": ["member-group", "admin-group", "editor-group"]})
123+
role = get_role_from_claims(
124+
{"groups": ["member-group", "admin-group", "editor-group"]}
125+
)
124126
assert role == ROLE_ADMIN
125127

126128
# Owner (4) should win over lower roles
127-
role = get_role_from_claims({"groups": ["member-group", "owner-group", "contributor-group"]})
129+
role = get_role_from_claims(
130+
{"groups": ["member-group", "owner-group", "contributor-group"]}
131+
)
128132
assert role == ROLE_OWNER
129133

130134
# Editor (3) should win over lower roles
131-
role = get_role_from_claims({"groups": ["member-group", "editor-group", "guest-group"]})
135+
role = get_role_from_claims(
136+
{"groups": ["member-group", "editor-group", "guest-group"]}
137+
)
132138
assert role == ROLE_EDITOR
133139

134140
def test_case_sensitivity(self):
@@ -140,8 +146,8 @@ def test_case_sensitivity(self):
140146

141147
with patch("gramps_webapi.auth.oidc.current_app", mock_app):
142148
assert get_role_from_claims({"groups": ["Admin-Group"]}) == ROLE_ADMIN
143-
assert get_role_from_claims({"groups": ["admin-group"]}) == ROLE_GUEST
144-
assert get_role_from_claims({"groups": ["ADMIN-GROUP"]}) == ROLE_GUEST
149+
assert get_role_from_claims({"groups": ["admin-group"]}) == ROLE_DISABLED
150+
assert get_role_from_claims({"groups": ["ADMIN-GROUP"]}) == ROLE_DISABLED
145151

146152
def test_custom_role_claim(self):
147153
"""Test using custom role claim (not 'groups')."""
@@ -165,11 +171,7 @@ def test_nested_role_claim(self):
165171
}.get(key, default)
166172

167173
with patch("gramps_webapi.auth.oidc.current_app", mock_app):
168-
user_claims = {
169-
"realm_access": {
170-
"roles": ["admin-role", "other-role"]
171-
}
172-
}
174+
user_claims = {"realm_access": {"roles": ["admin-role", "other-role"]}}
173175
role = get_role_from_claims(user_claims, role_claim="realm_access.roles")
174176
assert role == ROLE_ADMIN
175177

@@ -392,7 +394,9 @@ def test_existing_user_with_role_mapping(
392394
mock_app.config.get.return_value = "groups"
393395

394396
with patch("gramps_webapi.auth.oidc.current_app", mock_app):
395-
with patch("gramps_webapi.auth.oidc.get_role_from_claims", return_value=ROLE_EDITOR):
397+
with patch(
398+
"gramps_webapi.auth.oidc.get_role_from_claims", return_value=ROLE_EDITOR
399+
):
396400
result = create_or_update_oidc_user(userinfo, "test_tree", "google")
397401

398402
assert result == "existing-user-guid"
@@ -427,7 +431,9 @@ def test_existing_user_no_role_mapping(
427431
mock_app.config.get.return_value = "groups"
428432

429433
with patch("gramps_webapi.auth.oidc.current_app", mock_app):
430-
with patch("gramps_webapi.auth.oidc.get_role_from_claims", return_value=None):
434+
with patch(
435+
"gramps_webapi.auth.oidc.get_role_from_claims", return_value=None
436+
):
431437
result = create_or_update_oidc_user(userinfo, "test_tree", "google")
432438

433439
# Should not pass role parameter
@@ -446,8 +452,14 @@ def test_existing_user_no_role_mapping(
446452
@patch("gramps_webapi.auth.oidc.get_provider_config")
447453
@patch("gramps_webapi.auth.oidc.secrets.token_urlsafe")
448454
def test_new_user_google_provider(
449-
self, mock_token, mock_get_config,
450-
mock_create_oidc, mock_get_guid, mock_add_user, mock_get_user, mock_get_oidc
455+
self,
456+
mock_token,
457+
mock_get_config,
458+
mock_create_oidc,
459+
mock_get_guid,
460+
mock_add_user,
461+
mock_get_user,
462+
mock_get_oidc,
451463
):
452464
"""Test creating new user with Google provider."""
453465
mock_token.return_value = "random-password-123"
@@ -464,11 +476,18 @@ def test_new_user_google_provider(
464476
mock_app.config = {"TREE": "single_tree"}
465477

466478
with patch("gramps_webapi.auth.oidc.current_app", mock_app):
467-
with patch("gramps_webapi.auth.oidc.get_role_from_claims", return_value=ROLE_DISABLED):
479+
with patch(
480+
"gramps_webapi.auth.oidc.get_role_from_claims",
481+
return_value=ROLE_DISABLED,
482+
):
468483
# Mock get_tree_id and run_task to avoid database/task access in disabled role path
469-
with patch("gramps_webapi.api.util.get_tree_id", return_value="test_tree"):
484+
with patch(
485+
"gramps_webapi.api.util.get_tree_id", return_value="test_tree"
486+
):
470487
with patch("gramps_webapi.api.tasks.run_task"):
471-
result = create_or_update_oidc_user(userinfo, "test_tree", "google")
488+
result = create_or_update_oidc_user(
489+
userinfo, "test_tree", "google"
490+
)
472491

473492
assert result == "new-user-guid"
474493

@@ -491,8 +510,14 @@ def test_new_user_google_provider(
491510
@patch("gramps_webapi.auth.oidc.get_provider_config")
492511
@patch("gramps_webapi.auth.oidc.secrets.token_urlsafe")
493512
def test_new_user_custom_provider(
494-
self, mock_token, mock_get_config,
495-
mock_create_oidc, mock_get_guid, mock_add_user, mock_get_user, mock_get_oidc
513+
self,
514+
mock_token,
515+
mock_get_config,
516+
mock_create_oidc,
517+
mock_get_guid,
518+
mock_add_user,
519+
mock_get_user,
520+
mock_get_oidc,
496521
):
497522
"""Test creating new user with custom provider - no prefix."""
498523
mock_token.return_value = "random-password-123"
@@ -509,11 +534,18 @@ def test_new_user_custom_provider(
509534
mock_app.config = {"TREE": "single_tree"}
510535

511536
with patch("gramps_webapi.auth.oidc.current_app", mock_app):
512-
with patch("gramps_webapi.auth.oidc.get_role_from_claims", return_value=ROLE_DISABLED):
537+
with patch(
538+
"gramps_webapi.auth.oidc.get_role_from_claims",
539+
return_value=ROLE_DISABLED,
540+
):
513541
# Mock get_tree_id and run_task to avoid database/task access in disabled role path
514-
with patch("gramps_webapi.api.util.get_tree_id", return_value="test_tree"):
542+
with patch(
543+
"gramps_webapi.api.util.get_tree_id", return_value="test_tree"
544+
):
515545
with patch("gramps_webapi.api.tasks.run_task"):
516-
result = create_or_update_oidc_user(userinfo, None, PROVIDER_CUSTOM)
546+
result = create_or_update_oidc_user(
547+
userinfo, None, PROVIDER_CUSTOM
548+
)
517549

518550
# Username should NOT be prefixed for custom provider
519551
call_kwargs = mock_add_user.call_args[1]
@@ -527,8 +559,14 @@ def test_new_user_custom_provider(
527559
@patch("gramps_webapi.auth.oidc.get_provider_config")
528560
@patch("gramps_webapi.auth.oidc.secrets.token_urlsafe")
529561
def test_new_user_username_conflict_resolution(
530-
self, mock_token, mock_get_config,
531-
mock_create_oidc, mock_get_guid, mock_add_user, mock_get_user, mock_get_oidc
562+
self,
563+
mock_token,
564+
mock_get_config,
565+
mock_create_oidc,
566+
mock_get_guid,
567+
mock_add_user,
568+
mock_get_user,
569+
mock_get_oidc,
532570
):
533571
"""Test username conflict resolution with counter suffix."""
534572
mock_token.return_value = "random-password"
@@ -551,9 +589,14 @@ def test_new_user_username_conflict_resolution(
551589
mock_app.config = {"TREE": "single_tree"}
552590

553591
with patch("gramps_webapi.auth.oidc.current_app", mock_app):
554-
with patch("gramps_webapi.auth.oidc.get_role_from_claims", return_value=ROLE_DISABLED):
592+
with patch(
593+
"gramps_webapi.auth.oidc.get_role_from_claims",
594+
return_value=ROLE_DISABLED,
595+
):
555596
# Mock get_tree_id and run_task to avoid database/task access in disabled role path
556-
with patch("gramps_webapi.api.util.get_tree_id", return_value="test_tree"):
597+
with patch(
598+
"gramps_webapi.api.util.get_tree_id", return_value="test_tree"
599+
):
557600
with patch("gramps_webapi.api.tasks.run_task"):
558601
create_or_update_oidc_user(userinfo, None, "google")
559602

@@ -609,7 +652,9 @@ def test_oidc_enabled_no_providers(self, mock_get_providers, mock_oauth_class):
609652
@patch("gramps_webapi.auth.oidc.OAuth")
610653
@patch("gramps_webapi.auth.oidc.get_available_oidc_providers")
611654
@patch("gramps_webapi.auth.oidc.get_provider_config")
612-
def test_init_google_provider(self, mock_get_config, mock_get_providers, mock_oauth_class):
655+
def test_init_google_provider(
656+
self, mock_get_config, mock_get_providers, mock_oauth_class
657+
):
613658
"""Test initializing Google provider."""
614659
mock_app = MagicMock()
615660
mock_app.config.get.return_value = True
@@ -636,7 +681,9 @@ def test_init_google_provider(self, mock_get_config, mock_get_providers, mock_oa
636681
@patch("gramps_webapi.auth.oidc.OAuth")
637682
@patch("gramps_webapi.auth.oidc.get_available_oidc_providers")
638683
@patch("gramps_webapi.auth.oidc.get_provider_config")
639-
def test_init_github_provider(self, mock_get_config, mock_get_providers, mock_oauth_class):
684+
def test_init_github_provider(
685+
self, mock_get_config, mock_get_providers, mock_oauth_class
686+
):
640687
"""Test initializing GitHub provider (OAuth 2.0, not OIDC)."""
641688
mock_app = MagicMock()
642689
mock_app.config.get.return_value = True
@@ -657,13 +704,20 @@ def test_init_github_provider(self, mock_get_config, mock_get_providers, mock_oa
657704

658705
# Should use OAuth 2.0 registration (not OIDC)
659706
call_kwargs = mock_oauth.register.call_args[1]
660-
assert call_kwargs["access_token_url"] == "https://github.com/login/oauth/access_token"
661-
assert call_kwargs["authorize_url"] == "https://github.com/login/oauth/authorize"
707+
assert (
708+
call_kwargs["access_token_url"]
709+
== "https://github.com/login/oauth/access_token"
710+
)
711+
assert (
712+
call_kwargs["authorize_url"] == "https://github.com/login/oauth/authorize"
713+
)
662714

663715
@patch("gramps_webapi.auth.oidc.OAuth")
664716
@patch("gramps_webapi.auth.oidc.get_available_oidc_providers")
665717
@patch("gramps_webapi.auth.oidc.get_provider_config")
666-
def test_init_multiple_providers(self, mock_get_config, mock_get_providers, mock_oauth_class):
718+
def test_init_multiple_providers(
719+
self, mock_get_config, mock_get_providers, mock_oauth_class
720+
):
667721
"""Test initializing multiple providers."""
668722
mock_app = MagicMock()
669723
mock_app.config.get.return_value = True
@@ -672,12 +726,27 @@ def test_init_multiple_providers(self, mock_get_config, mock_get_providers, mock
672726
# Return different configs for each provider
673727
def get_config_side_effect(provider_id, app=None):
674728
configs = {
675-
"google": {"name": "Google", "client_id": "g-id", "client_secret": "g-secret",
676-
"issuer": "https://accounts.google.com", "scopes": "openid email"},
677-
"microsoft": {"name": "Microsoft", "client_id": "ms-id", "client_secret": "ms-secret",
678-
"issuer": "https://login.microsoftonline.com/common/v2.0", "scopes": "openid email"},
679-
PROVIDER_CUSTOM: {"name": "Custom", "client_id": "c-id", "client_secret": "c-secret",
680-
"issuer": "https://custom.com", "scopes": "openid email"},
729+
"google": {
730+
"name": "Google",
731+
"client_id": "g-id",
732+
"client_secret": "g-secret",
733+
"issuer": "https://accounts.google.com",
734+
"scopes": "openid email",
735+
},
736+
"microsoft": {
737+
"name": "Microsoft",
738+
"client_id": "ms-id",
739+
"client_secret": "ms-secret",
740+
"issuer": "https://login.microsoftonline.com/common/v2.0",
741+
"scopes": "openid email",
742+
},
743+
PROVIDER_CUSTOM: {
744+
"name": "Custom",
745+
"client_id": "c-id",
746+
"client_secret": "c-secret",
747+
"issuer": "https://custom.com",
748+
"scopes": "openid email",
749+
},
681750
}
682751
return configs.get(provider_id)
683752

0 commit comments

Comments
 (0)