Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move Backends to this repo #21

Merged
merged 1 commit into from
Mar 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions tahoe_sites/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""
Authentication backends for Tahoe's multi-tenancy.
"""
from django.contrib.auth.backends import AllowAllUsersModelBackend, ModelBackend
from organizations.models import Organization

from tahoe_sites.api import is_main_site, get_current_site, get_organization_by_site, get_organization_for_user


class DefaultSiteBackend(ModelBackend):
"""
User can log in to the default/root site (edx.appsembler.com) because it is required during the signup.
Also, superusers (appsembler admins) can log into any site.
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
Authenticate superusers only.
"""
user = super().authenticate(request, username, password, **kwargs)

if user:
if user.is_superuser or is_main_site(get_current_site()):
return user
return None


class OrganizationMemberBackend(AllowAllUsersModelBackend):
"""Backend for organization based authentication

This class is an extension of Django's `AllowAllUserModelBackend`

This class checks that the user to authenticate belongs to one of the
organizations in the specified site

This class extends `AllowAllUserModelBackend` instead of `ModelBackend`
because users need to be able to authenticate when the `user.is_active` is
`False`. The reason for this is that Open edX has an email verification
scheme that uses `User.is_active` in order to prevent user activity until
the users have verified their email. Effectively, edx-platform LMS user
authentication state is used to manage authorization state.

The key problem is that Django's `django.contrib.auth.backends.ModelBackend`
is called with `not user.is_active` and Ironwood introduced a check to test
authentication on a not yet authorized user. This breaks Tahoe multisite
behavior. Extending `AllowAllUsersModelBackend` restores correct behavior.

For further reference, see settings files and read Django documentation for
`AUTHENTICATION_BACKENDS`
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
Authenticate organization learners.
"""
user = super().authenticate(request, username, password, **kwargs)

result = None
site = get_current_site()
if not is_main_site(site) and user and not user.is_superuser:
try:
# `get_organization_for_user` never return `None` but raises DoesNotExist if no organization is found
user_organization = get_organization_for_user(user=user, fail_if_inactive=False)
if get_organization_by_site(site=site) == user_organization:
result = user
except Organization.DoesNotExist:
# Don't fail if the user is not a member. Just prevent authentication
pass

return result
2 changes: 1 addition & 1 deletion tahoe_sites/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Tests for models
Tests for APIs
"""
# pylint: disable=too-many-public-methods

Expand Down
129 changes: 129 additions & 0 deletions tahoe_sites/tests/test_backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""
Tests for Backend
"""
from unittest import mock

import ddt
from django.conf import settings
from django.test import TestCase

from tahoe_sites.api import create_tahoe_site
from tahoe_sites.backends import DefaultSiteBackend, OrganizationMemberBackend
from tahoe_sites.tests.fatories import UserFactory
from tahoe_sites.tests.utils import create_organization_mapping


class MixinTestBackendBase(TestCase):
shadinaif marked this conversation as resolved.
Show resolved Hide resolved
"""
Mixin to hold test defaults
"""
backend_class = None

def setUp(self):
"""
Initialization
"""
self.dummy_password = '123123'
self.user = UserFactory()
self.user.set_password(self.dummy_password)
self.user.save()
self.request = None

def exec_auth(self):
"""
Execute backend_class().authenticate method

:return: result of authenticate method
"""
with mock.patch('crum.get_current_request', return_value=self.request):
return self.backend_class().authenticate( # pylint: disable=not-callable
request=self.request,
username=self.user.username,
password=self.dummy_password
)


@ddt.ddt
class TestDefaultSiteBackend(MixinTestBackendBase):
"""
Tests for DefaultSiteBackend
"""
backend_class = DefaultSiteBackend

@ddt.data(
(False, None, False),
(False, 99, False),
(False, settings.SITE_ID, True),
(True, None, True),
(True, 99, True),
(True, settings.SITE_ID, True),
)
@ddt.unpack
def test_with_user(self, is_superuser, site_id, success_expected):
"""
Verify that the backend allows/disallows authentication correctly according to is_superuser and current site
"""
self.user.is_superuser = is_superuser
self.user.save()
self.request = mock.Mock(site=mock.Mock(id=site_id))

self.assertEqual(self.exec_auth(), self.user if success_expected else None)

@ddt.data(None, 99, settings.SITE_ID,)
def test_no_user(self, site_id):
"""
Verify that the backend disallows authentication without a user
"""
self.user.username = None
self.dummy_password = None
self.request = mock.Mock(site=mock.Mock(id=site_id))

self.assertIsNone(self.exec_auth())


@ddt.ddt
class TestOrganizationMemberBackend(MixinTestBackendBase):
"""
Tests for OrganizationMemberBackend
"""
backend_class = OrganizationMemberBackend

@ddt.data(
(True, True),
(True, False),
(False, True),
(False, False),
)
@ddt.unpack
def test_member_and_no_member(self, is_active, is_member):
"""
Verify that the backend allows/disallows authentication correctly according to the user being a member of
the organization related to the site (regardless of being active or not)
"""
info = create_tahoe_site(domain='test.org', short_name='TO')
self.request = mock.Mock(site=info['site'])

self.user.is_active = is_active
self.user.save()
if is_member:
create_organization_mapping(self.user, info['organization'])

self.assertEqual(self.exec_auth(), self.user if is_member else None)

def test_superuser(self):
"""
Verify that the backend disallows authentication when the user is superusers
"""
self.user.is_superuser = True
self.user.save()
self.request = mock.Mock(site=mock.Mock(id=99))

self.assertIsNone(self.exec_auth())

def test_default_site(self):
"""
Verify that the backend disallows authentication in default site
"""
self.request = mock.Mock(site=mock.Mock(id=settings.SITE_ID))

self.assertIsNone(self.exec_auth())