diff --git a/.github/workflows/scripts/before_install.sh b/.github/workflows/scripts/before_install.sh index f8ff33dbf30..cc05d14d6dc 100755 --- a/.github/workflows/scripts/before_install.sh +++ b/.github/workflows/scripts/before_install.sh @@ -79,6 +79,11 @@ services: image: "docker.io/pulp/pulp-fixtures:latest" env: BASE_URL: "http://pulp-fixtures:8080" + - name: "saml2-idp" + image: "ghcr.io/pfrest/mock-saml2-idp:latest" + env: + SP_ENTITY_ID: "http://pulp" + SP_ACS_LOCATION: "http://pulp/saml/acs/" VARSYAML if [ "$TEST" = "s3" ]; then diff --git a/ci_requirements.txt b/ci_requirements.txt index 8b137891791..268d042d873 100644 --- a/ci_requirements.txt +++ b/ci_requirements.txt @@ -1 +1 @@ - +pulpcore[saml2] diff --git a/pulpcore/app/settings.py b/pulpcore/app/settings.py index d34aebf8a17..8bdc63d3f95 100644 --- a/pulpcore/app/settings.py +++ b/pulpcore/app/settings.py @@ -610,7 +610,7 @@ def saml2_settings_hook(settings): if "SAML_CONFIG" in settings: data["INSTALLED_APPS"] = ["djangosaml2"] data["MIDDLEWARE"] = ["djangosaml2.middleware.SamlSessionMiddleware"] - data["AUTHENTICATION_BACKENDS"] = ["djangosaml2.backends.Saml2Backend"] + data["AUTHENTICATION_BACKENDS"] = ["pulpcore.saml2.backends.PulpSaml2Backend"] if "LOGIN_URL" not in settings: data["LOGIN_URL"] = "/saml2/login/" if "SESSION_COOKIE_SECURE" not in settings: diff --git a/pulpcore/saml2/backends.py b/pulpcore/saml2/backends.py new file mode 100644 index 00000000000..11557ceda9f --- /dev/null +++ b/pulpcore/saml2/backends.py @@ -0,0 +1,72 @@ +import sys +from logging import getLogger + +from djangosaml2.backends import Saml2Backend + +from pulpcore.app.models import Domain +from pulpcore.app.models.role import Role +from pulpcore.app.util import resolve_prn + + +_logger = getLogger(__name__) + + +def _parse_role_assignment(role_assignment): + try: + _logger.debug("Considering role-assignment '%s'.", role_assignment) + # TODO Is '/' a good choice here? + role_name, domain_name, obj_prn = role_assignment.split("/") # ':' is used in PRNs + role = Role.objects.get(name=role_name) + domain = None if domain_name == "" else Domain.objects.get(name=domain_name) + obj = None if obj_prn == "" else resolve_prn(obj_prn) + except Exception as e: + _logger.warning( + "Could not sync role-assignment '%s' from saml2 attributes.", + role_assignment, + exc_info=sys.exc_info(), + ) + return None + return role, domain, obj + + +class PulpSaml2Backend(Saml2Backend): + def _update_user( + self, user, attributes: dict, attribute_mapping: dict, force_save: bool = False + ): + if "pulp_roles" in attributes: + _logger.debug("Sync role assignments for user '%s'.", user.username) + role_assignments = ( + item + for item in map(_parse_role_assignment, attributes["pulp_roles"]) + if item is not None + ) + + if user.pk is not None: + # Adjust roles for existing user. + assignment_pks = [] + for role, domain, obj in role_assignments: + if obj is None: + content_type = None + obj_pk = None + else: + content_type = 1 + obj_pk = obj.pk + user_role = user.object_roles.filter( + role=role, domain=domain, content_type=content_type, object_id=obj_pk + ).first() + if user_role is None: + user_role = user.object_roles.create( + role=role, domain=domain, content_object=obj + ) + _logger.debug("Created.") + else: + _logger.debug("Found.") + assignment_pks.append(user_role.pk) + user.object_roles.exclude(pk__in=assignment_pks).delete() + else: + user.save() + _logger.debug("New user object; create all role assignments.") + for role, domain, obj in role_assignments: + user.object_roles.create(role=role, domain=domain, content_object=obj) + + return super()._update_user(user, attributes, attribute_mapping, force_save)