Skip to content

Commit

Permalink
feat: add BearerAuthentication to API and improve EoxTenantAPIPermiss…
Browse files Browse the repository at this point in the history
…ion checks (#149)
  • Loading branch information
magajh committed Sep 20, 2022
1 parent d9a0937 commit e8803ea
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 22 deletions.
61 changes: 57 additions & 4 deletions eox_tenant/api/v1/permissions.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,66 @@
"""
Permission for eox_tenant api v1.
"""
from rest_framework import permissions
from django.conf import settings
from django.contrib.auth.models import Permission, User
from django.contrib.contenttypes.models import ContentType
from django.db.utils import ProgrammingError
from rest_framework import exceptions, permissions


def load_permissions():
"""
Helper method to load a custom permission on DB that will be
used to give access to the eox-tenant API.
"""
if settings.EOX_TENANT_LOAD_PERMISSIONS:
try:
content_type = ContentType.objects.get_for_model(User)
Permission.objects.get_or_create( # pylint: disable=unused-variable
codename='can_call_eox_tenant',
name='Can access eox-tenant API',
content_type=content_type,
)
except ProgrammingError:
# This code runs when the app is loaded, if a migration has not been
# done a ProgrammingError exception is raised.
# we are bypassing those cases to let migrations run smoothly.
pass


class EoxTenantAPIPermission(permissions.BasePermission):
"""Only allows super user."""
"""
Defines a custom permissions to access eox-tenant API.
These permissions make sure that a token is created with the client credentials of the same site is
being used on, and the user has a valid SignUp source for the site.
"""

def has_permission(self, request, view):
if request.user.is_superuser:
"""
To grant access, checks if the requesting user:
1) it's a staff user
3) it's calling the API from a site authorized by the auth application or client
4) has can call eox-tenant API permission
"""
user = request.user

if user.is_staff:
return True
return False

try:
application_uri_allowed = request.auth.application.redirect_uri_allowed(request.build_absolute_uri('/'))
except Exception: # pylint: disable=broad-except
application_uri_allowed = False

try:
client_url_allowed = request.get_host() in request.auth.client.url
except Exception: # pylint: disable=broad-except
client_url_allowed = False

if client_url_allowed or application_uri_allowed:
return user.has_perm('auth.can_call_eox_tenant')

# If we get here either someone is using a token created on one site in a different site
# or there was a missconfiguration of the oauth client.
# we return the most basic message to prevent leaking important information .
raise exceptions.NotAuthenticated(detail="Invalid token")
6 changes: 0 additions & 6 deletions eox_tenant/api/v1/tests/test_microsites.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,3 @@ def test_delete_microsite(self, _):
response = self.client.delete(self.url_detail)

self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

def test_permissions_microsite(self):
"""Must return 403, only allows superuser."""
response = self.client.get(self.url)

self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
83 changes: 83 additions & 0 deletions eox_tenant/api/v1/tests/test_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Test module for the permissions class
"""
from django.test import TestCase
from mock import MagicMock
from rest_framework.exceptions import NotAuthenticated

from eox_tenant.api.v1.permissions import EoxTenantAPIPermission


class EoxTenantAPIPermissionTest(TestCase):
""" Test cases for the EoxTenantAPIPermission class."""

def test_permissions_for_staff(self):
""" Staff always passes."""
request = MagicMock()
request.user.is_staff = True

has_perm = EoxTenantAPIPermission().has_permission(request, MagicMock())

self.assertTrue(has_perm)

def test_read_user_permissions(self):
""" If the auth does not fail, it comes down to check the
domain in the client or application.
Expected behavior:
has_permission method returns False
"""
request = MagicMock()
request.user.is_staff = False
request.user.has_perm.return_value = False

has_perm = EoxTenantAPIPermission().has_permission(request, MagicMock())

self.assertFalse(has_perm)

def test_permissions_without_auth(self):
""" If anything in the auth fails, the NotAuthenticated exception is raised
Expected behavior:
NotAuthenticated exception is raised
"""
request = MagicMock()
request.user.is_staff = False
request.user.has_perm.return_value = False
request.auth = None

with self.assertRaises(NotAuthenticated):
EoxTenantAPIPermission().has_permission(request, MagicMock())

def test_permissions_auth_dop(self):
""" Authorize the domain via the client.url.
Expected behavior:
has_permission method returns False
"""
request = MagicMock()
request.user.is_staff = False
request.user.has_perm.return_value = True
request.get_host.return_value = "domain.com"
request.auth.client.url = "https://domain.com/"

has_perm = EoxTenantAPIPermission().has_permission(request, MagicMock())

self.assertTrue(has_perm)

def test_permissions_auth_dot(self):
""" Authorize the domain via the application.allowed_uris
Expected behavior:
has_permission method returns False
"""
request = MagicMock()
request.user.is_staff = False
request.user.has_perm.return_value = True
request.auth.application.redirect_uri_allowed.return_value = True

has_perm = EoxTenantAPIPermission().has_permission(request, MagicMock())

self.assertTrue(has_perm)
6 changes: 0 additions & 6 deletions eox_tenant/api/v1/tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,3 @@ def test_valid_patch_tenant_config(self, _):
response = self.client.patch(self.url_detail, data=data, format='json')

self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_permissions_route(self):
"""Must return 403, only allows superuser."""
response = self.client.get(self.url)

self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
6 changes: 0 additions & 6 deletions eox_tenant/api/v1/tests/test_tenant_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,3 @@ def test_delete_tenant_config(self, _):
response = self.client.delete(self.url_detail)

self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

def test_permissions_tenant_config(self):
"""Must return 403, only allows superuser."""
response = self.client.get(self.url)

self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
5 changes: 5 additions & 0 deletions eox_tenant/api/v1/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
API v1 viewsets.
"""
from rest_framework import viewsets
from rest_framework.authentication import SessionAuthentication
from rest_framework.parsers import JSONParser

from eox_tenant.api.v1.permissions import EoxTenantAPIPermission
from eox_tenant.api.v1.serializers import MicrositeSerializer, RouteSerializer, TenantConfigSerializer
from eox_tenant.edxapp_wrapper.bearer_authentication import BearerAuthentication
from eox_tenant.models import Microsite, Route, TenantConfig


class MicrositeViewSet(viewsets.ModelViewSet):
"""MicrositeViewSet that allows the basic API actions."""

authentication_classes = (BearerAuthentication, SessionAuthentication)
parser_classes = [JSONParser]
permission_classes = [EoxTenantAPIPermission]
serializer_class = MicrositeSerializer
Expand All @@ -21,6 +24,7 @@ class MicrositeViewSet(viewsets.ModelViewSet):
class TenantConfigViewSet(viewsets.ModelViewSet):
"""TenantConfigViewSet that allows the basic API actions."""

authentication_classes = (BearerAuthentication, SessionAuthentication)
parser_classes = [JSONParser]
permission_classes = [EoxTenantAPIPermission]
serializer_class = TenantConfigSerializer
Expand All @@ -30,6 +34,7 @@ class TenantConfigViewSet(viewsets.ModelViewSet):
class RouteViewSet(viewsets.ModelViewSet):
"""RouteViewSet that allows the basic API actions."""

authentication_classes = (BearerAuthentication, SessionAuthentication)
parser_classes = [JSONParser]
permission_classes = [EoxTenantAPIPermission]
serializer_class = RouteSerializer
Expand Down
4 changes: 4 additions & 0 deletions eox_tenant/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ def ready(self):
"""
Method to perform actions after apps registry is ended
"""
from eox_tenant.api.v1.permissions import \
load_permissions as load_api_permissions # pylint: disable=import-outside-toplevel
load_api_permissions()

from eox_tenant.permissions import load_permissions # pylint: disable=import-outside-toplevel
load_permissions()

Expand Down
16 changes: 16 additions & 0 deletions eox_tenant/edxapp_wrapper/backends/bearer_authentication_l_v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Backend for authentication.
This file contains all the necessary authentication dependencies from
https://github.com/eduNEXT/edunext-platform/tree/master/openedx/core/lib/api/authentication.py
"""
from openedx.core.lib.api.authentication import BearerAuthentication # pylint: disable=import-error


def get_bearer_authentication():
"""Allow to get the function BearerAuthentication from
https://github.com/eduNEXT/edunext-platform/tree/master/openedx/core/lib/api/authentication.py
Returns:
BearerAuthentication function.
"""
return BearerAuthentication
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
""" Backend test abstraction. """


def get_bearer_authentication():
"""Allow to get the function BearerAuthentication from
https://github.com/eduNEXT/edunext-platform/tree/master/openedx/core/lib/api/authentication.py
Returns:
BearerAuthentication function.
"""
try:
from openedx.core.lib.api.authentication import BearerAuthentication # pylint: disable=import-outside-toplevel
except ImportError:
BearerAuthentication = object
return BearerAuthentication
17 changes: 17 additions & 0 deletions eox_tenant/edxapp_wrapper/bearer_authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
Bearer Authentication definition.
"""
from importlib import import_module

from django.conf import settings


def get_bearer_authentication():
""" Gets BearerAuthentication class. """
backend_function = settings.EOX_TENANT_BEARER_AUTHENTICATION
backend = import_module(backend_function)

return backend.get_bearer_authentication()


BearerAuthentication = get_bearer_authentication() # pylint: disable=invalid-name
1 change: 1 addition & 0 deletions eox_tenant/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def plugin_settings(settings):
settings.GET_THEMING_HELPERS = 'eox_tenant.edxapp_wrapper.backends.theming_helpers_h_v1'
settings.EOX_TENANT_EDX_AUTH_BACKEND = "eox_tenant.edxapp_wrapper.backends.edx_auth_i_v1"
settings.EOX_TENANT_USERS_BACKEND = 'eox_tenant.edxapp_wrapper.backends.users_l_v1'
settings.EOX_TENANT_BEARER_AUTHENTICATION = 'eox_tenant.edxapp_wrapper.backends.bearer_authentication_l_v1'
settings.EOX_MAX_CONFIG_OVERRIDE_SECONDS = 300
settings.EDXMAKO_MODULE_BACKEND = 'eox_tenant.edxapp_wrapper.backends.edxmako_l_v1'
settings.UTILS_MODULE_BACKEND = 'eox_tenant.edxapp_wrapper.backends.util_h_v1'
Expand Down
1 change: 1 addition & 0 deletions eox_tenant/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class SettingsClass:
GET_SITE_CONFIGURATION_MODULE = 'eox_tenant.edxapp_wrapper.backends.site_configuration_module_test_v1'
GET_THEMING_HELPERS = 'eox_tenant.edxapp_wrapper.backends.theming_helpers_test_v1'
EOX_TENANT_USERS_BACKEND = 'eox_tenant.edxapp_wrapper.backends.users_test_v1'
EOX_TENANT_BEARER_AUTHENTICATION = 'eox_tenant.edxapp_wrapper.backends.bearer_authentication_test_v1'

COURSE_KEY_PATTERN = r'(?P<course_key_string>[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+)'
COURSE_ID_PATTERN = COURSE_KEY_PATTERN.replace('course_key_string', 'course_id')
Expand Down

0 comments on commit e8803ea

Please sign in to comment.