-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(oauth2_provider): check for OAuth2ScopePermission on all APIViews
Add a check for OAuth2ScopePermission on all active views in an application. - ANSIBLE_BASE_OAUTH2_PROVIDER_PERMISSIONS_CHECK_IGNORED_VIEWS django setting for setting ignores by class path. - Uses django's check framework, should be compatible with the related functionality. - Set as a deployment check, so it will not block app startup. Attach OAuth2ScopePermission to all necessary views with permission_classes defined when ansible_base.oauth2_provider is installed. AAP-26507
- Loading branch information
1 parent
595937b
commit 4844691
Showing
17 changed files
with
430 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
from typing import Type | ||
|
||
from rest_framework.schemas.generators import EndpointEnumerator | ||
from rest_framework.views import APIView | ||
|
||
|
||
def get_api_view_functions(urlpatterns=None) -> set[Type[APIView]]: | ||
""" | ||
Extract view classes from a urlpatterns list using the show_urls helper functions | ||
:param urlpatterns: django urlpatterns list | ||
:return: set of all view classes used by the urlpatterns list | ||
""" | ||
views = set() | ||
|
||
enumerator = EndpointEnumerator() | ||
# Get all active APIViews from urlconf | ||
endpoints = enumerator.get_api_endpoints(patterns=urlpatterns) | ||
for _, _, func in endpoints: | ||
# ApiView.as_view() breadcrumb | ||
if hasattr(func, 'cls'): | ||
views.add(func.cls) | ||
|
||
return views |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,14 @@ | ||
from django.apps import AppConfig | ||
from django.core.checks import register | ||
|
||
|
||
class Oauth2ProviderConfig(AppConfig): | ||
default_auto_field = 'django.db.models.BigAutoField' | ||
name = 'ansible_base.oauth2_provider' | ||
label = 'dab_oauth2_provider' | ||
|
||
def ready(self): | ||
# Load checks | ||
from ansible_base.oauth2_provider.checks.permisssions_check import oauth2_permission_scope_check | ||
|
||
register(oauth2_permission_scope_check, "oauth2_permissions", deploy=True) |
Empty file.
153 changes: 153 additions & 0 deletions
153
ansible_base/oauth2_provider/checks/permisssions_check.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
from typing import Iterable, Optional, Type, Union | ||
|
||
from django.apps import AppConfig | ||
from django.conf import settings | ||
from django.core.checks import CheckMessage, Debug, Error, Warning | ||
from rest_framework.permissions import AllowAny, OperandHolder, OperationHolderMixin, SingleOperandHolder | ||
from rest_framework.views import APIView | ||
|
||
from ansible_base.lib.utils.views.urls import get_api_view_functions | ||
from ansible_base.oauth2_provider.permissions import OAuth2ScopePermission | ||
|
||
|
||
class OAuth2ScopePermissionCheck: | ||
""" | ||
Class containing logic for checking view classes for the | ||
OAuth2ScopePermission permission_class, and aggregating | ||
CheckMessage's for django system checks. | ||
:param ignore_list: List of python import path strings for view classes exempt from the check logic. | ||
:type ignore_list: list | ||
""" | ||
|
||
def __init__(self, ignore_list: Iterable[str], generate_check_messages=True): | ||
self.messages: list[CheckMessage] = [] | ||
self.current_view: Optional[Type[APIView]] = None | ||
self.ignore_list = ignore_list | ||
self.generate_check_messages = generate_check_messages | ||
|
||
def check_message(self, message: CheckMessage): | ||
if self.generate_check_messages: | ||
self.messages.append(message) | ||
|
||
# These are all warning or error conditions, this function is mostly saying to not invert OAuth2ScopePermissions | ||
# Returns False. | ||
def process_single_operand_holder(self, operand_holder: SingleOperandHolder) -> bool: | ||
# The only unary operand for permission classes provided by rest_framework is ~ (not) | ||
if self.parse_permission_class(operand_holder.op1_class): | ||
self.check_message( | ||
Warning( | ||
"~ (not) operand used on OAuth2ScopePermission, probably a bad idea.", | ||
id="ansible_base.oauth2_provider.W001", | ||
obj=self.current_view, | ||
) | ||
) | ||
|
||
return False | ||
|
||
def process_operand_holder(self, operand_holder: OperandHolder) -> bool: | ||
return self.parse_permission_class(operand_holder.op1_class) or self.parse_permission_class(operand_holder.op2_class) | ||
|
||
# Check if permission class is present in nested operands | ||
# Sort of recursive? Reasonably this should not be an issue, so long as we don't recurse on an unknown OperationHolderMixin type | ||
def parse_permission_class(self, cls: Union[Type[OperationHolderMixin], OperationHolderMixin]) -> bool: | ||
# First, most likely case, we're dealing with a BasePermission subclass. | ||
if cls is OAuth2ScopePermission: | ||
return True | ||
elif isinstance(cls, SingleOperandHolder): | ||
# Warning or Error case: Will not accept OAuth2 permission nested in NOT | ||
return self.process_single_operand_holder(cls) | ||
elif isinstance(cls, OperandHolder): | ||
return self.process_operand_holder(cls) | ||
return False | ||
|
||
def check_view(self, view_class: Type[APIView]) -> bool: | ||
""" | ||
Primary function of the OAuth2ScopePermissionCheck. | ||
Checks if OAuth2ScopePermission is present on the supplied view's | ||
permission_classes; ignores classes that are not APIViews, or that are | ||
in the ignore_list. | ||
Appends CheckMessages to self.messages as a side effect. | ||
:param view_class: django View class or rest_framework ApiView class | ||
:return: True if view_class uses the OAuth2ScopePermission permission | ||
class, or has some mitigating circumstance that prohibits it, such as | ||
view_class not using permission classes, or its import path being in | ||
self.ignore_list; returns False otherwise. | ||
""" | ||
if f"{view_class.__module__}.{view_class.__name__}" in self.ignore_list: | ||
self.check_message( | ||
Debug( | ||
"View class in the ignore list. Ignoring.", | ||
obj=view_class, | ||
id="ansible_base.oauth2_provider.D03", | ||
) | ||
) | ||
return True | ||
|
||
self.current_view = view_class | ||
|
||
for permission_class in getattr(self.current_view, "permission_classes", []): | ||
if self.parse_permission_class(permission_class): | ||
self.check_message( | ||
Debug( | ||
"Found OAuth2ScopePermission permission_class", | ||
obj=self.current_view, | ||
id="ansible_base.oauth2_provider.D02", | ||
) | ||
) | ||
return True | ||
|
||
if not self.current_view.permission_classes or AllowAny in self.current_view.permission_classes: | ||
self.check_message( | ||
Debug( | ||
"View object is fully permissive, OAuth2ScopePermission is not required", | ||
obj=self.current_view, | ||
id="ansible_base.oauth2_provider.D04", | ||
) | ||
) | ||
return True | ||
|
||
# if we went though the whole loop without finding a valid permission_class, raise an error | ||
self.check_message( | ||
Error( | ||
"View class has no valid usage of OAuth2ScopePermission", | ||
obj=self.current_view, | ||
id="ansible_base.oauth2_provider.E002", | ||
) | ||
) | ||
return False | ||
|
||
|
||
def view_in_app_configs(view_class: type, app_configs: Optional[list[AppConfig]]) -> bool: | ||
if app_configs: | ||
for app_config in app_configs: | ||
if view_class.__module__.startswith(app_config.name): | ||
return True | ||
return False | ||
return True | ||
|
||
|
||
def oauth2_permission_scope_check(app_configs: Optional[list[AppConfig]], **kwargs) -> list[CheckMessage]: | ||
""" | ||
Check for OAuth2ScopePermission permission class on all enabled views. | ||
Ignore views in the ANSIBLE_BASE_OAUTH2_PROVIDER_PERMISSIONS_CHECK_IGNORED_VIEWS setting | ||
""" | ||
ignore_list = set( | ||
getattr(settings, "ANSIBLE_BASE_OAUTH2_PROVIDER_PERMISSIONS_CHECK_DEFAULT_IGNORED_VIEWS", []) | ||
+ getattr(settings, "ANSIBLE_BASE_OAUTH2_PROVIDER_PERMISSIONS_CHECK_IGNORED_VIEWS", []) | ||
) | ||
|
||
check = OAuth2ScopePermissionCheck(ignore_list) | ||
|
||
view_functions = get_api_view_functions() | ||
for view in view_functions: | ||
# Only run checks on included apps (or all if app_configs is None) | ||
if view_in_app_configs(view, app_configs): | ||
check.check_view(view) | ||
|
||
return check.messages |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.