Ctrl K
ComunidadGuías…

tests/saml/test_saml_adapter.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import base64
from types import SimpleNamespace
from unittest.mock import patch

from allauth.socialaccount.models import SocialAccount, SocialApp, SocialLogin
from allauth.socialaccount.signals import social_account_updated
from django.contrib.sessions.backends.db import SessionStore
from django.test import RequestFactory, TestCase

from files.tests import create_account, fixture_path
from identity_providers.models import (
    IdentityProviderGlobalRole,
    IdentityProviderGroupRole,
    IdentityProviderUserLog,
)
from rbac.models import RBACGroup, RBACMembership
from saml_auth.adapter import (
    SAMLAccountAdapter,
    add_user_logo,
    handle_role_mapping,
    handle_saml_logs_save,
    perform_user_actions,
)
from saml_auth.models import SAMLConfiguration
from users.models import User

PROVIDER_ID = "adapter-idp"


def make_user(username, **kwargs):
    return create_account(username=username, email=f"{username}@example.com", password="saml-adapter-password", **kwargs)


def make_social_app(provider_id=PROVIDER_ID):
    return SocialApp.objects.create(provider="saml", provider_id=provider_id, name=f"{provider_id} name", client_id=provider_id)


def make_configuration(social_app, **overrides):
    fields = {
        "social_app": social_app,
        "sso_url": "https://idp.example.org/sso",
        "slo_url": "https://idp.example.org/slo",
        "sp_metadata_url": "https://sp.example.org/saml/metadata",
        "idp_id": f"https://idp.example.org/{social_app.provider_id}",
        "idp_cert": "dummy-cert",
        "uid": "uid",
        "groups": "isMemberOf",
        "role": "affiliation",
    }
    fields.update(overrides)
    return SAMLConfiguration.objects.create(**fields)


def png_base64():
    with open(fixture_path("test_image.png"), "rb") as fp:
        return base64.b64encode(fp.read()).decode()


class PopulateUserTest(TestCase):
    def setUp(self):
        self.adapter = SAMLAccountAdapter()

    def sociallogin(self, uid):
        return SimpleNamespace(user=User(), account=SimpleNamespace(uid=uid), data=None)

    def test_username_keeps_only_reversible_characters(self):
        sociallogin = self.sociallogin("jane doe/éx+1@uni.edu")
        user = self.adapter.populate_user(None, sociallogin, {})
        self.assertEqual(user.username, "jane_doe__x_1@uni.edu")

    def test_username_is_truncated_to_field_length(self):
        user = self.adapter.populate_user(None, self.sociallogin("a" * 300), {})
        self.assertEqual(len(user.username), 150)

    def test_missing_uid_leaves_username_empty(self):
        user = self.adapter.populate_user(None, self.sociallogin(None), {})
        self.assertEqual(user.username, "")

    def test_name_fields_are_copied_and_data_is_kept_on_sociallogin(self):
        sociallogin = self.sociallogin("jdoe")
        data = {"name": "Jane Doe", "first_name": "Jane", "last_name": "", "email": "jane@example.com"}
        user = self.adapter.populate_user(None, sociallogin, data)
        self.assertEqual((user.name, user.first_name, user.last_name), ("Jane Doe", "Jane", ""))
        self.assertIs(sociallogin.data, data)

    def test_signup_is_always_open(self):
        self.assertTrue(self.adapter.is_open_for_signup(None, None))


class SaveUserTest(TestCase):
    def test_new_user_is_saved_and_mapped_to_groups_from_response(self):
        social_app = make_social_app()
        make_configuration(social_app)
        group = RBACGroup.objects.create(name="Physics", uid="physics", identity_provider=social_app)

        user = User(username="new_saml_user", email="new_saml_user@example.com")
        account = SocialAccount(provider=PROVIDER_ID, uid="new_saml_user", extra_data={"isMemberOf": ["physics"], "affiliation": ["member"]})
        sociallogin = SocialLogin(user=user, account=account)
        request = RequestFactory().get("/")
        request.session = SessionStore()

        saved = SAMLAccountAdapter().save_user(request, sociallogin)

        self.assertIsNotNone(saved.pk)
        self.assertFalse(saved.has_usable_password())
        self.assertTrue(SocialAccount.objects.filter(user=saved, uid="new_saml_user").exists())
        self.assertTrue(RBACMembership.objects.filter(user=saved, rbac_group=group, role="member").exists())


class SocialAccountUpdatedTest(TestCase):
    def test_signal_refreshes_changed_profile_fields(self):
        user = make_user("saml_updated_user", name="Old Name")
        account = SimpleNamespace(provider="unknown-provider", extra_data={})
        sociallogin = SimpleNamespace(user=user, account=account, data={"name": "New Name", "email": "fresh@example.com", "first_name": ""})

        social_account_updated.send(sender=SocialLogin, request=None, sociallogin=sociallogin)

        user.refresh_from_db()
        self.assertEqual(user.name, "New Name")
        self.assertEqual(user.email, "fresh@example.com")


class PerformUserActionsTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.social_app = make_social_app()

    def account(self, extra_data):
        return SimpleNamespace(provider=PROVIDER_ID, extra_data=extra_data)

    def test_unchanged_fields_are_not_saved(self):
        user = make_user("saml_same_user", name="Same")
        with patch.object(User, "save") as save:
            perform_user_actions(user, self.account({}), {"name": "Same", "email": user.email})
        save.assert_not_called()

    def test_without_configuration_no_mapping_or_log_happens(self):
        user = make_user("saml_noconf_user")
        RBACGroup.objects.create(name="Unused", uid="unused", identity_provider=self.social_app)
        perform_user_actions(user, self.account({"isMemberOf": ["unused"]}))
        self.assertFalse(RBACMembership.objects.filter(user=user).exists())
        self.assertFalse(IdentityProviderUserLog.objects.exists())

    def test_with_configuration_response_is_logged_without_photo(self):
        make_configuration(self.social_app)
        user = make_user("saml_logged_user")
        perform_user_actions(user, self.account({"uid": ["saml_logged_user"], "jpegPhoto": [png_base64()]}))
        log = IdentityProviderUserLog.objects.get(user=user, identity_provider=self.social_app)
        self.assertIn("saml_logged_user", log.logs)
        self.assertNotIn("jpegPhoto", log.logs)
        self.assertIn("SAML Log - saml_logged_user", str(log))

    def test_logging_can_be_disabled_on_configuration(self):
        make_configuration(self.social_app, save_saml_response_logs=False)
        user = make_user("saml_unlogged_user")
        perform_user_actions(user, self.account({"uid": ["x"]}))
        self.assertFalse(IdentityProviderUserLog.objects.exists())

    def test_unknown_provider_is_tolerated(self):
        user = make_user("saml_unknown_provider_user")
        result = perform_user_actions(user, SimpleNamespace(provider="nobody", extra_data={}))
        self.assertIs(result, user)

    def test_logs_save_strips_photo_from_stored_response(self):
        user = make_user("saml_strip_user")
        extra_data = {"mail": ["a@example.com"], "jpegPhoto": ["abc"]}
        self.assertTrue(handle_saml_logs_save(user, extra_data, self.social_app))
        self.assertNotIn("jpegPhoto", extra_data)
        self.assertEqual(IdentityProviderUserLog.objects.get(user=user).logs, str({"mail": ["a@example.com"]}))


class AddUserLogoTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.social_app = make_social_app()

    def test_default_photo_attribute_sets_logo_for_user_without_one(self):
        user = make_user("saml_logo_user")
        self.assertEqual(user.logo.name, "userlogos/user.jpg")
        add_user_logo(user, {"jpegPhoto": [png_base64()]})
        user.refresh_from_db()
        self.assertNotEqual(user.logo.name, "userlogos/user.jpg")
        self.assertTrue(user.logo.name.startswith("userlogos/"))

    def test_configured_attribute_name_is_used(self):
        configuration = make_configuration(self.social_app, user_logo="thumbnailPhoto")
        user = make_user("saml_custom_logo_user")
        add_user_logo(user, {"jpegPhoto": [png_base64()]}, configuration)
        user.refresh_from_db()
        self.assertEqual(user.logo.name, "userlogos/user.jpg")
        add_user_logo(user, {"thumbnailPhoto": [png_base64()]}, configuration)
        user.refresh_from_db()
        self.assertNotEqual(user.logo.name, "userlogos/user.jpg")

    def test_existing_custom_logo_is_not_overwritten(self):
        user = make_user("saml_has_logo_user")
        User.objects.filter(pk=user.pk).update(logo="userlogos/custom.jpg")
        user.refresh_from_db()
        add_user_logo(user, {"jpegPhoto": [png_base64()]})
        user.refresh_from_db()
        self.assertEqual(user.logo.name, "userlogos/custom.jpg")

    def test_undecodable_photo_is_logged_and_ignored(self):
        user = make_user("saml_bad_logo_user")
        with self.assertLogs(level="ERROR"):
            self.assertTrue(add_user_logo(user, {"jpegPhoto": ["not base64!"]}))
        user.refresh_from_db()
        self.assertEqual(user.logo.name, "userlogos/user.jpg")


class HandleRoleMappingTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.social_app = make_social_app()
        cls.other_app = make_social_app("adapter-other-idp")
        cls.physics = RBACGroup.objects.create(name="Physics", uid="physics", identity_provider=cls.social_app)
        cls.maths = RBACGroup.objects.create(name="Maths", uid="maths", identity_provider=cls.social_app)
        cls.foreign = RBACGroup.objects.create(name="Physics", uid="physics", identity_provider=cls.other_app)

    def setUp(self):
        self.configuration = make_configuration(self.social_app)
        self.user = make_user("saml_role_user")

    def roles(self):
        return set(RBACMembership.objects.filter(user=self.user).values_list("rbac_group__uid", "rbac_group__identity_provider__provider_id", "role"))

    def test_without_configuration_nothing_happens(self):
        self.assertFalse(handle_role_mapping(self.user, {"isMemberOf": ["physics"]}, self.social_app, None))
        self.assertEqual(self.roles(), set())

    def test_groups_of_this_provider_get_member_role_by_default(self):
        self.assertTrue(handle_role_mapping(self.user, {"isMemberOf": ["physics", "unknown"]}, self.social_app, self.configuration))
        self.assertEqual(self.roles(), {("physics", PROVIDER_ID, "member")})

    def test_role_attribute_naming_a_group_role_is_used_directly(self):
        handle_role_mapping(self.user, {"isMemberOf": ["physics"], "affiliation": ["manager", "member"]}, self.social_app, self.configuration)
        self.assertEqual(self.roles(), {("physics", PROVIDER_ID, "manager")})

    def test_group_role_mapping_translates_idp_role(self):
        IdentityProviderGroupRole.objects.create(identity_provider=self.social_app, name="faculty", map_to="contributor")
        handle_role_mapping(self.user, {"isMemberOf": ["physics", "maths"], "affiliation": ["faculty"]}, self.social_app, self.configuration)
        self.assertEqual(self.roles(), {("physics", PROVIDER_ID, "contributor"), ("maths", PROVIDER_ID, "contributor")})

    def test_unmapped_role_falls_back_to_member(self):
        handle_role_mapping(self.user, {"isMemberOf": ["physics"], "affiliation": "alumnus"}, self.social_app, self.configuration)
        self.assertEqual(self.roles(), {("physics", PROVIDER_ID, "member")})

    def test_global_role_mapping_updates_user_flags(self):
        IdentityProviderGlobalRole.objects.create(identity_provider=self.social_app, name="staff", map_to="editor")
        handle_role_mapping(self.user, {"affiliation": ["staff"]}, self.social_app, self.configuration)
        self.user.refresh_from_db()
        self.assertTrue(self.user.is_editor)
        self.assertFalse(self.user.is_superuser)

    def test_existing_membership_role_is_updated(self):
        RBACMembership.objects.create(user=self.user, rbac_group=self.physics, role="member")
        handle_role_mapping(self.user, {"isMemberOf": ["physics"], "affiliation": ["contributor"]}, self.social_app, self.configuration)
        self.assertEqual(self.roles(), {("physics", PROVIDER_ID, "contributor")})

    def test_groups_missing_from_response_are_kept_by_default(self):
        RBACMembership.objects.create(user=self.user, rbac_group=self.maths, role="member")
        handle_role_mapping(self.user, {"isMemberOf": ["physics"]}, self.social_app, self.configuration)
        self.assertEqual(self.roles(), {("physics", PROVIDER_ID, "member"), ("maths", PROVIDER_ID, "member")})

    def test_remove_from_groups_drops_only_this_providers_missing_groups(self):
        self.configuration.remove_from_groups = True
        self.configuration.save()
        RBACMembership.objects.create(user=self.user, rbac_group=self.maths, role="member")
        RBACMembership.objects.create(user=self.user, rbac_group=self.foreign, role="member")
        handle_role_mapping(self.user, {"isMemberOf": ["physics"]}, self.social_app, self.configuration)
        self.assertEqual(self.roles(), {("physics", PROVIDER_ID, "member"), ("physics", "adapter-other-idp", "member")})

    def test_errors_while_reading_role_mappings_still_assign_groups(self):
        with patch.object(User, "set_role_from_mapping", side_effect=RuntimeError("boom")):
            IdentityProviderGlobalRole.objects.create(identity_provider=self.social_app, name="manager", map_to="manager")
            with self.assertLogs(level="ERROR"):
                handle_role_mapping(self.user, {"isMemberOf": ["physics"], "affiliation": ["manager"]}, self.social_app, self.configuration)
        self.assertEqual(self.roles(), {("physics", PROVIDER_ID, "manager")})