Skip to content

Commit 64c3aef

Browse files
committed
Add test that views have oauth scope thing
1 parent 401c3ba commit 64c3aef

File tree

1 file changed

+157
-0
lines changed

1 file changed

+157
-0
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Generated by Claude Sonnet 4
2+
3+
from django.core.checks import ERROR
4+
5+
from ansible_base.oauth2_provider.checks.permisssions_check import oauth2_permission_scope_check
6+
7+
8+
def test_all_ansible_base_views_have_oauth2_scope_permission():
9+
"""
10+
Test that all ansible_base views (especially rbac/service_api and resource_registry)
11+
have OAuth2ScopePermission when checked against the real URL configuration.
12+
13+
This complements the mocked tests in test_permissions_check.py by ensuring
14+
production views are covered against the real URL configuration.
15+
"""
16+
from django.core.checks import ERROR
17+
18+
from ansible_base.oauth2_provider.checks.permisssions_check import oauth2_permission_scope_check
19+
20+
# Call the check function against real URLs (no mocking)
21+
messages = oauth2_permission_scope_check(None)
22+
23+
# Filter to only error messages (missing OAuth2ScopePermission)
24+
error_messages = [m for m in messages if m.level == ERROR]
25+
26+
# Filter to ansible_base views only
27+
ansible_base_errors = []
28+
for error in error_messages:
29+
view_class = error.obj
30+
if view_class.__module__.startswith('ansible_base'):
31+
ansible_base_errors.append(f"{view_class.__module__}.{view_class.__name__}")
32+
33+
# If there are any ansible_base views missing OAuth2ScopePermission, fail with complete list
34+
if ansible_base_errors:
35+
error_list = "\n".join(f" - {view}" for view in sorted(ansible_base_errors))
36+
assert False, f"The following ansible_base views do not have OAuth2ScopePermission:\n{error_list}"
37+
38+
39+
def test_ansible_base_views_allowing_write_operations_have_oauth2_scope_permission():
40+
"""
41+
Test that ansible_base views allowing destructive operations (POST, PUT, PATCH, DELETE)
42+
have OAuth2ScopePermission. This is critical because read-only tokens should not be able
43+
to perform write operations.
44+
45+
Focuses on views that could allow a read-only token to perform writes if OAuth2ScopePermission
46+
is missing.
47+
"""
48+
# Call the check function against real URLs (no mocking)
49+
messages = oauth2_permission_scope_check(None)
50+
51+
# Filter to only error messages (missing OAuth2ScopePermission)
52+
error_messages = [m for m in messages if m.level == ERROR]
53+
54+
# Debug: Print all ansible_base errors and their write capability
55+
ansible_base_errors = []
56+
debug_info = []
57+
for error in error_messages:
58+
view_class = error.obj
59+
if not view_class.__module__.startswith('ansible_base'):
60+
continue
61+
62+
view_name = f"{view_class.__module__}.{view_class.__name__}"
63+
allows_writes = _view_allows_write_operations(view_class)
64+
debug_info.append(f"{view_name} - allows_writes: {allows_writes}")
65+
66+
if allows_writes:
67+
ansible_base_errors.append(view_name)
68+
69+
print("\nDEBUG: All ansible_base views missing OAuth2ScopePermission:")
70+
for info in debug_info:
71+
print(f" {info}")
72+
73+
# Filter to ansible_base views that allow write operations
74+
write_capable_errors = ansible_base_errors
75+
76+
# If there are any write-capable ansible_base views missing OAuth2ScopePermission, fail
77+
if write_capable_errors:
78+
error_list = "\n".join(f" - {view}" for view in sorted(write_capable_errors))
79+
assert False, f"The following ansible_base views allow write operations but do not have OAuth2ScopePermission:\n{error_list}"
80+
81+
82+
def _view_allows_write_operations(view_class):
83+
"""
84+
Determine if a view class allows write operations (POST, PUT, PATCH, DELETE).
85+
86+
Returns True if the view could potentially allow destructive operations.
87+
"""
88+
from rest_framework import mixins
89+
from rest_framework.viewsets import ViewSet
90+
91+
# Check if it's a standard ViewSet (which typically allows all operations)
92+
if issubclass(view_class, ViewSet) and not hasattr(view_class, 'queryset'):
93+
# Standard ViewSet - check if it has write methods defined
94+
write_methods = {'post', 'put', 'patch', 'delete', 'create', 'update', 'partial_update', 'destroy'}
95+
view_methods = {method.lower() for method in dir(view_class)}
96+
if write_methods.intersection(view_methods):
97+
return True
98+
99+
# Check for DRF mixins that enable write operations
100+
write_mixins = [
101+
mixins.CreateModelMixin,
102+
mixins.UpdateModelMixin,
103+
mixins.DestroyModelMixin,
104+
]
105+
106+
for mixin in write_mixins:
107+
if issubclass(view_class, mixin):
108+
return True
109+
110+
# Check for custom action decorators that might allow POST
111+
if hasattr(view_class, 'get_extra_actions'):
112+
try:
113+
actions = view_class.get_extra_actions()
114+
for action in actions:
115+
# Check if action allows POST/PUT/PATCH/DELETE
116+
if hasattr(action, 'methods'):
117+
methods = [m.upper() for m in action.methods]
118+
if any(method in ['POST', 'PUT', 'PATCH', 'DELETE'] for method in methods):
119+
return True
120+
except Exception:
121+
# If we can't determine actions, err on the side of caution
122+
pass
123+
124+
# If we can't determine or it only has read methods, assume it's read-only
125+
return False
126+
127+
128+
def test_missing_permission_detection():
129+
"""
130+
Test that the test correctly identifies views missing OAuth2ScopePermission.
131+
This test temporarily patches a view to remove the permission and ensures it gets detected.
132+
"""
133+
import copy
134+
from unittest import mock
135+
136+
from django.core.checks import ERROR
137+
from rest_framework.permissions import IsAuthenticated
138+
139+
from ansible_base.oauth2_provider.checks.permisssions_check import oauth2_permission_scope_check
140+
from test_app.views import UserViewSet
141+
142+
# Create a patched version of UserViewSet without OAuth2ScopePermission
143+
PatchedUserViewSet = copy.deepcopy(UserViewSet)
144+
PatchedUserViewSet.permission_classes = [IsAuthenticated] # Remove OAuth2ScopePermission
145+
146+
# Mock the UserViewSet in test_app.views to use our patched version
147+
with mock.patch("test_app.views.UserViewSet", PatchedUserViewSet):
148+
# Call the check function
149+
messages = oauth2_permission_scope_check(None)
150+
151+
# Filter to only error messages
152+
error_messages = [m for m in messages if m.level == ERROR]
153+
154+
# Check that our patched UserViewSet is detected as missing the permission
155+
user_viewset_errors = [error for error in error_messages if error.obj.__name__ == 'UserViewSet' and 'test_app' in error.obj.__module__]
156+
157+
assert len(user_viewset_errors) == 1, f"Expected exactly 1 error for test_app.views.UserViewSet, but got {len(user_viewset_errors)}"

0 commit comments

Comments
 (0)