diff --git a/pyproject.toml b/pyproject.toml index b55e28c74..55763f89c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,6 +94,7 @@ ignore_errors = true [[tool.mypy.overrides]] module = [ "tests.integration.test_integration", + "tests.storage_service.test_oidc", ] ignore_errors = false diff --git a/storage_service/common/backends.py b/storage_service/common/backends.py index 42e0f35c7..acfa031d4 100644 --- a/storage_service/common/backends.py +++ b/storage_service/common/backends.py @@ -1,7 +1,10 @@ import json +from typing import Any +from typing import Dict from administration import roles from django.conf import settings +from django.contrib.auth.models import User from django_cas_ng.backends import CASBackend from josepy.jws import JWS from mozilla_django_oidc.auth import OIDCAuthenticationBackend @@ -21,14 +24,16 @@ def configure_user(self, user): class CustomOIDCBackend(OIDCAuthenticationBackend): """Provide OpenID Connect authentication.""" - def get_userinfo(self, access_token, id_token, verified_id): + def get_userinfo( + self, access_token: str, id_token: str, verified_id: Dict[str, Any] + ) -> Dict[str, Any]: """Extract user details from JSON web tokens. It returns a dict of user details that will be applied directly to the user model. """ - def decode_token(token): + def decode_token(token: str) -> Any: sig = JWS.from_compact(token.encode("utf-8")) payload = sig.payload.decode("utf-8") return json.loads(payload) @@ -36,7 +41,7 @@ def decode_token(token): access_info = decode_token(access_token) id_info = decode_token(id_token) - info = {} + info: Dict[str, Any] = {} for oidc_attr, user_attr in settings.OIDC_ACCESS_ATTRIBUTE_MAP.items(): if oidc_attr in access_info: @@ -48,18 +53,18 @@ def decode_token(token): return info - def create_user(self, user_info): + def create_user(self, user_info: Dict[str, Any]) -> User: user = super().create_user(user_info) for attr, value in user_info.items(): setattr(user, attr, value) self.set_user_role(user) return user - def update_user(self, user, user_info): + def update_user(self, user: User, user_info: Dict[str, Any]) -> User: self.set_user_role(user) return user - def set_user_role(self, user): + def set_user_role(self, user: User) -> None: # TODO: use user claims accessible via user's authentication tokens. role = roles.promoted_role(roles.USER_ROLE_READER) user.set_role(role) diff --git a/tests/storage_service/test_oidc.py b/tests/storage_service/test_oidc.py index c13aea3ad..88d5f015d 100644 --- a/tests/storage_service/test_oidc.py +++ b/tests/storage_service/test_oidc.py @@ -1,52 +1,97 @@ import pytest +import pytest_django from administration import roles from common.backends import CustomOIDCBackend -from django.conf import settings -from django.test import TestCase -from django.test import override_settings - - -@pytest.mark.skipif( - not settings.OIDC_AUTHENTICATION, reason="tests will only pass if OIDC is enabled" -) -class TestOIDC(TestCase): - def test_create_user(self): - backend = CustomOIDCBackend() - user = backend.create_user( - {"email": "test@example.com", "first_name": "Test", "last_name": "User"} - ) - - user.refresh_from_db() - assert user.first_name == "Test" - assert user.last_name == "User" - assert user.email == "test@example.com" - assert user.username == "test@example.com" - assert user.get_role() == roles.USER_ROLE_MANAGER - - @override_settings(DEFAULT_USER_ROLE=roles.USER_ROLE_REVIEWER) - def test_create_demoted_user(self): - """The role given to a new user is based on ``DEFAULT_USER_ROLE``. - - In this test, we're ensuring that new users are given the reviewer role - instead of the default "manager" role. - """ - backend = CustomOIDCBackend() - user = backend.create_user( - {"email": "test@example.com", "first_name": "Test", "last_name": "User"} - ) - - user.refresh_from_db() - assert user.get_role() == roles.USER_ROLE_REVIEWER - - def test_get_userinfo(self): - # Encoded at https://www.jsonwebtoken.io/ - # {"email": "test@example.com"} - id_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20iLCJqdGkiOiI1M2QyMzUzMy04NDk0LTQyZWQtYTJiZC03Mzc2MjNmMjUzZjciLCJpYXQiOjE1NzMwMzE4NDQsImV4cCI6MTU3MzAzNTQ0NH0.m3nHgvj_DyVJMcW5eyYuUss1Y0PNzJV2O3bX0b_DCmI" - # {"given_name": "Test", "family_name": "User"} - access_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJnaXZlbl9uYW1lIjoiVGVzdCIsImZhbWlseV9uYW1lIjoiVXNlciIsImp0aSI6ImRhZjIwNTNiLWE4MTgtNDE1Yy1hM2Y1LTkxYWVhMTMxYjljZCIsImlhdCI6MTU3MzAzMTk3OSwiZXhwIjoxNTczMDM1NTc5fQ.cGcmt7d9IuKndvrqPpAH3Dvb3KyCOMqixUWgS7sg8r4" - - backend = CustomOIDCBackend() - info = backend.get_userinfo(access_token, id_token, None) - assert info["email"] == "test@example.com" - assert info["first_name"] == "Test" - assert info["last_name"] == "User" +from django.contrib.auth.models import User + + +@pytest.fixture +def settings( + settings: pytest_django.fixtures.SettingsWrapper, +) -> pytest_django.fixtures.SettingsWrapper: + settings.OIDC_OP_TOKEN_ENDPOINT = "https://example.com/token" + settings.OIDC_OP_USER_ENDPOINT = "https://example.com/user" + settings.OIDC_RP_CLIENT_ID = "rp_client_id" + settings.OIDC_RP_CLIENT_SECRET = "rp_client_secret" + settings.OIDC_ACCESS_ATTRIBUTE_MAP = { + "given_name": "first_name", + "family_name": "last_name", + } + settings.OIDC_ID_ATTRIBUTE_MAP = {"email": "email"} + settings.OIDC_USERNAME_ALGO = lambda email: email + + return settings + + +@pytest.mark.django_db +def test_create_user(settings: pytest_django.fixtures.SettingsWrapper) -> None: + backend = CustomOIDCBackend() + + user = backend.create_user( + {"email": "test@example.com", "first_name": "Test", "last_name": "User"} + ) + + user.refresh_from_db() + assert user.first_name == "Test" + assert user.last_name == "User" + assert user.email == "test@example.com" + assert user.username == "test@example.com" + assert user.get_role() == roles.USER_ROLE_READER + + +@pytest.mark.django_db +def test_create_demoted_user(settings: pytest_django.fixtures.SettingsWrapper) -> None: + """The role given to a new user is based on ``DEFAULT_USER_ROLE``. + + In this test, we're ensuring that new users are given the reviewer role + instead of the default "manager" role. + """ + settings.DEFAULT_USER_ROLE = roles.USER_ROLE_REVIEWER + backend = CustomOIDCBackend() + + user = backend.create_user( + {"email": "test@example.com", "first_name": "Test", "last_name": "User"} + ) + + user.refresh_from_db() + assert user.get_role() == roles.USER_ROLE_REVIEWER + + +@pytest.mark.django_db +def test_get_userinfo(settings: pytest_django.fixtures.SettingsWrapper) -> None: + # Encoded at https://www.jsonwebtoken.io/ + # {"email": "test@example.com"} + id_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20iLCJqdGkiOiI1M2QyMzUzMy04NDk0LTQyZWQtYTJiZC03Mzc2MjNmMjUzZjciLCJpYXQiOjE1NzMwMzE4NDQsImV4cCI6MTU3MzAzNTQ0NH0.m3nHgvj_DyVJMcW5eyYuUss1Y0PNzJV2O3bX0b_DCmI" + # {"given_name": "Test", "family_name": "User"} + access_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJnaXZlbl9uYW1lIjoiVGVzdCIsImZhbWlseV9uYW1lIjoiVXNlciIsImp0aSI6ImRhZjIwNTNiLWE4MTgtNDE1Yy1hM2Y1LTkxYWVhMTMxYjljZCIsImlhdCI6MTU3MzAzMTk3OSwiZXhwIjoxNTczMDM1NTc5fQ.cGcmt7d9IuKndvrqPpAH3Dvb3KyCOMqixUWgS7sg8r4" + backend = CustomOIDCBackend() + + info = backend.get_userinfo(access_token, id_token, {}) + + assert info["email"] == "test@example.com" + assert info["first_name"] == "Test" + assert info["last_name"] == "User" + + +@pytest.mark.django_db +def test_update_user(settings: pytest_django.fixtures.SettingsWrapper) -> None: + """The role given to a new user is based on ``DEFAULT_USER_ROLE``. + + In this test, we're ensuring that updating a user promotes it to a new role. + """ + + user = User.objects.create( + first_name="Foo", last_name="Bar", username="foobar", email="foobar@example.com" + ) + # User has been given the DEFAULT_USER_ROLE on creation. + assert user.get_role() == roles.USER_ROLE_READER + backend = CustomOIDCBackend() + + # Promote the role in the DEFAULT_USER_ROLE setting. + settings.DEFAULT_USER_ROLE = roles.USER_ROLE_ADMIN + + backend.update_user(user, {}) + + user.refresh_from_db() + # User has been promoted to the new role on update. + assert user.get_role() == roles.USER_ROLE_ADMIN