From c9448a0bc90d09b5d520b84f6d47d8c98d5bc0ab Mon Sep 17 00:00:00 2001 From: Alex Manning Date: Tue, 18 Jun 2024 16:40:16 +0100 Subject: [PATCH 1/2] Impove swagger generation. --- jasmin_services/api/views.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/jasmin_services/api/views.py b/jasmin_services/api/views.py index a804481..1b4a968 100644 --- a/jasmin_services/api/views.py +++ b/jasmin_services/api/views.py @@ -175,6 +175,12 @@ def get_queryset(self): @drf_spectacular.utils.extend_schema_view( list=drf_spectacular.utils.extend_schema( parameters=[ + drf_spectacular.utils.OpenApiParameter( + name="user_username", + required=True, + type=str, + location=drf_spectacular.utils.OpenApiParameter.PATH, + ), drf_spectacular.utils.OpenApiParameter( name="service", required=False, @@ -204,6 +210,11 @@ class UserGrantsViewSet(rf_mixins.ListModelMixin, rf_viewsets.GenericViewSet): filterset_class = filters.UserGrantsFilter def get_queryset(self): + # If we are generating swagger definitions, return the correct + # queryset to allow types to be infered without error. + if getattr(self, "swagger_fake_view", False): + return models.Grant.objects.none() + queryset = models.Grant.objects.filter( access__user__username=self.kwargs["user_username"], revoked=False, From 2a066db63884505de6ab97ae52fd1365b27dd38d Mon Sep 17 00:00:00 2001 From: Alex Manning Date: Mon, 24 Jun 2024 17:58:13 +0100 Subject: [PATCH 2/2] Make LDAP groups an unmanaged model. --- jasmin_services/models/__init__.py | 46 ++++++++++- jasmin_services/models/behaviours/__init__.py | 43 ++--------- jasmin_services/models/behaviours/ldap.py | 74 ------------------ jasmin_services/models/ldap.py | 77 +++++++++++++++++++ 4 files changed, 126 insertions(+), 114 deletions(-) create mode 100644 jasmin_services/models/ldap.py diff --git a/jasmin_services/models/__init__.py b/jasmin_services/models/__init__.py index a9c7bc8..3526890 100644 --- a/jasmin_services/models/__init__.py +++ b/jasmin_services/models/__init__.py @@ -1,10 +1,13 @@ -""" -Module defining models for the JASMIN services app. -""" +"""Module defining models for the JASMIN services app.""" __author__ = "Matt Pryor" __copyright__ = "Copyright 2015 UK Science and Technology Facilities Council" +import logging +import sys + +import django.conf + from .access import Access from .behaviours import * from .category import Category @@ -13,6 +16,8 @@ from .role import Role, RoleObjectPermission from .service import Service +logger = logging.getLogger(__name__) + __all__ = [ "Access", "Category", @@ -23,3 +28,38 @@ "RoleObjectPermission", "Service", ] + +# LDAP behaviour is only available if the jamsin-ldap optional dependency is installed. +try: + from .ldap import Group +except ImportError: + logger.warning("LDAP is not enabled. Install optional dependencies to activate.") +else: + __all__.append("Group") + # Concrete models for the LDAP groups as defined in settings + this_module = sys.modules[__name__] + for grp in django.conf.settings.JASMIN_SERVICES["LDAP_GROUPS"]: + __all__.append(grp["MODEL_NAME"]) + setattr( + this_module, + grp["MODEL_NAME"], + type( + grp["MODEL_NAME"], + (Group,), + { + "__module__": __name__, + "Meta": type( + "Meta", + (Group.Meta,), + { + "verbose_name": grp["VERBOSE_NAME"], + "verbose_name_plural": grp.get("VERBOSE_NAME_PLURAL"), + "managed": False, + }, + ), + "base_dn": grp["BASE_DN"], + "gid_number_min": grp["GID_NUMBER_MIN"], + "gid_number_max": grp["GID_NUMBER_MAX"], + }, + ), + ) diff --git a/jasmin_services/models/behaviours/__init__.py b/jasmin_services/models/behaviours/__init__.py index ea845a0..460df38 100644 --- a/jasmin_services/models/behaviours/__init__.py +++ b/jasmin_services/models/behaviours/__init__.py @@ -1,9 +1,6 @@ """Module defining specific category implementations.""" import logging -import sys - -import django.conf from .base import Behaviour # unimport:skip from .mail import JoinJISCMailListBehaviour # unimport:skip @@ -11,6 +8,11 @@ __all__ = [] logger = logging.getLogger(__name__) +__all__ += [ + "Behaviour", + "JoinJISCMailListBehaviour", +] + # Keycloak behaviour is only available if the keycloak optional dependency is installed. try: from .keycloak import KeycloakAttributeBehaviour @@ -19,46 +21,13 @@ else: __all__ += ["KeycloakAttributeBehaviour"] -__all__ += [ - "Behaviour", - "JoinJISCMailListBehaviour", -] - # LDAP behaviour is only available if the jamsin-ldap optional dependency is installed. try: - from .ldap import Group, LdapGroupBehaviour, LdapTagBehaviour # unimport:skip + from .ldap import LdapGroupBehaviour, LdapTagBehaviour # unimport:skip except ImportError: logger.warning("LDAP Behaviour is not enabled. Install optional dependencies to activate.") else: __all__ += [ "LdapTagBehaviour", - "Group", "LdapGroupBehaviour", ] - - # Concrete models for the LDAP groups as defined in settings - this_module = sys.modules[__name__] - for grp in django.conf.settings.JASMIN_SERVICES["LDAP_GROUPS"]: - __all__.append(grp["MODEL_NAME"]) - setattr( - this_module, - grp["MODEL_NAME"], - type( - grp["MODEL_NAME"], - (Group,), - { - "__module__": __name__, - "Meta": type( - "Meta", - (Group.Meta,), - { - "verbose_name": grp["VERBOSE_NAME"], - "verbose_name_plural": grp.get("VERBOSE_NAME_PLURAL"), - }, - ), - "base_dn": grp["BASE_DN"], - "gid_number_min": grp["GID_NUMBER_MIN"], - "gid_number_max": grp["GID_NUMBER_MAX"], - }, - ), - ) diff --git a/jasmin_services/models/behaviours/ldap.py b/jasmin_services/models/behaviours/ldap.py index 1cc6574..0b97124 100644 --- a/jasmin_services/models/behaviours/ldap.py +++ b/jasmin_services/models/behaviours/ldap.py @@ -5,7 +5,6 @@ import django.core.exceptions import django.core.validators import django.db.models -import jasmin_ldap_django.models from .base import Behaviour @@ -39,79 +38,6 @@ def __str__(self): return f"LDAP Tag <{self.tag}>" -class Group(jasmin_ldap_django.models.LDAPModel): - """Abstract base class for a posixGroup in LDAP.""" - - class GidAllocationFailed(RuntimeError): - """Raised when a gid allocation fails.""" - - class Meta(jasmin_ldap_django.models.LDAPModel.Meta): - abstract = True - ordering = ["name"] - - object_classes = ["top", "posixGroup"] - search_classes = ["posixGroup"] - - # User visible fields - name = jasmin_ldap_django.models.CharField( - db_column="cn", - primary_key=True, - max_length=50, - validators=[ - django.core.validators.RegexValidator( - regex="^[a-zA-Z]", - message="Name must start with a letter.", - ), - django.core.validators.RegexValidator( - regex="[a-zA-Z0-9]$", - message="Name must end with a letter or number.", - ), - django.core.validators.RegexValidator( - regex="^[a-zA-Z0-9_-]+$", - message="Name must contain letters, numbers, _ and -.", - ), - ], - error_messages={ - "unique": "Name is already in use.", - "max_length": "Name must have at most %(limit_value)d characters.", - }, - ) - description = jasmin_ldap_django.models.TextField(db_column="description", blank=True) - member_uids = jasmin_ldap_django.models.ListField(db_column="memberUid", blank=True) - - # blank = True is set here for the field validation, but a blank gidNumber is - # not allowed by the save method - gidNumber = jasmin_ldap_django.models.PositiveIntegerField(unique=True, blank=True) - - def __str__(self): - return f"cn={self.name},{self.base_dn}" - - def save(self, *args, **kwargs): - # If there is no gidNumber, try to allocate one - if self.gidNumber is None: - # Get the max gidNumber in our allowed range that is currently in use - max_gid = ( - self.__class__.objects.filter(gidNumber__isnull=False) - .filter(gidNumber__lt=self.gid_number_max) - .aggregate(max_gid=django.db.models.Max("gidNumber")) - .get("max_gid") - ) - if max_gid is not None: - # Use the next gidNumber, but make sure we are in the current range - next_gid = max(max_gid + 1, self.gid_number_min) - else: - # If there is no max, then this is the first record with a gidNumber - # so use the minimum - next_gid = self.gid_number_min - # If we were unable to allocate a gid in the range, report it - # We use a non-field error in case the gidNumber field is not being - # displayed - if next_gid >= self.gid_number_max: - raise self.GidAllocationFailed() - self.gidNumber = next_gid - return super().save(*args, **kwargs) - - class LdapGroupBehaviour(Behaviour): """Behaviour for adding a user to an LDAP group.""" diff --git a/jasmin_services/models/ldap.py b/jasmin_services/models/ldap.py new file mode 100644 index 0000000..ba23240 --- /dev/null +++ b/jasmin_services/models/ldap.py @@ -0,0 +1,77 @@ +"""Model to use LDAP groups.""" +import django.core.validators +import django.db.models +import jasmin_ldap_django.models + + +class Group(jasmin_ldap_django.models.LDAPModel): + """Abstract base class for a posixGroup in LDAP.""" + + class GidAllocationFailed(RuntimeError): + """Raised when a gid allocation fails.""" + + class Meta(jasmin_ldap_django.models.LDAPModel.Meta): + abstract = True + ordering = ["name"] + + object_classes = ["top", "posixGroup"] + search_classes = ["posixGroup"] + + # User visible fields + name = jasmin_ldap_django.models.CharField( + db_column="cn", + primary_key=True, + max_length=50, + validators=[ + django.core.validators.RegexValidator( + regex="^[a-zA-Z]", + message="Name must start with a letter.", + ), + django.core.validators.RegexValidator( + regex="[a-zA-Z0-9]$", + message="Name must end with a letter or number.", + ), + django.core.validators.RegexValidator( + regex="^[a-zA-Z0-9_-]+$", + message="Name must contain letters, numbers, _ and -.", + ), + ], + error_messages={ + "unique": "Name is already in use.", + "max_length": "Name must have at most %(limit_value)d characters.", + }, + ) + description = jasmin_ldap_django.models.TextField(db_column="description", blank=True) + member_uids = jasmin_ldap_django.models.ListField(db_column="memberUid", blank=True) + + # blank = True is set here for the field validation, but a blank gidNumber is + # not allowed by the save method + gidNumber = jasmin_ldap_django.models.PositiveIntegerField(unique=True, blank=True) + + def __str__(self): + return f"cn={self.name},{self.base_dn}" + + def save(self, *args, **kwargs): + # If there is no gidNumber, try to allocate one + if self.gidNumber is None: + # Get the max gidNumber in our allowed range that is currently in use + max_gid = ( + self.__class__.objects.filter(gidNumber__isnull=False) + .filter(gidNumber__lt=self.gid_number_max) + .aggregate(max_gid=django.db.models.Max("gidNumber")) + .get("max_gid") + ) + if max_gid is not None: + # Use the next gidNumber, but make sure we are in the current range + next_gid = max(max_gid + 1, self.gid_number_min) + else: + # If there is no max, then this is the first record with a gidNumber + # so use the minimum + next_gid = self.gid_number_min + # If we were unable to allocate a gid in the range, report it + # We use a non-field error in case the gidNumber field is not being + # displayed + if next_gid >= self.gid_number_max: + raise self.GidAllocationFailed() + self.gidNumber = next_gid + return super().save(*args, **kwargs)