Skip to content

Commit

Permalink
Make LDAP groups an unmanaged model.
Browse files Browse the repository at this point in the history
  • Loading branch information
amanning9 committed Jun 24, 2024
1 parent c9448a0 commit 2a066db
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 114 deletions.
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)

0 comments on commit 2a066db

Please sign in to comment.