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

Fix ldap group migrations #143

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
11 changes: 11 additions & 0 deletions jasmin_services/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
46 changes: 43 additions & 3 deletions jasmin_services/models/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,6 +16,8 @@
from .role import Role, RoleObjectPermission
from .service import Service

logger = logging.getLogger(__name__)

__all__ = [
"Access",
"Category",
Expand All @@ -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"],
},
),
)
43 changes: 6 additions & 37 deletions jasmin_services/models/behaviours/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
"""Module defining specific category implementations."""

import logging
import sys

import django.conf

from .base import Behaviour # unimport:skip
from .mail import JoinJISCMailListBehaviour # unimport:skip

__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
Expand All @@ -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"],
},
),
)
74 changes: 0 additions & 74 deletions jasmin_services/models/behaviours/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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."""

Expand Down
77 changes: 77 additions & 0 deletions jasmin_services/models/ldap.py
Original file line number Diff line number Diff line change
@@ -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)
Loading