Skip to content

Commit

Permalink
Merge pull request #34 from alan-turing-institute/33-groups-of-groups
Browse files Browse the repository at this point in the history
Add groups of groups
  • Loading branch information
jemrobinson authored Apr 5, 2024
2 parents 0df82fc + 18522d6 commit 83c1bfa
Show file tree
Hide file tree
Showing 21 changed files with 460 additions and 172 deletions.
61 changes: 60 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ Each user will have an entry like
```ldif
dn: CN=<user name>,OU=users,DC=<your domain>
objectClass: inetOrgPerson
objectClass: inetuser
objectClass: organizationalPerson
objectClass: person
objectClass: posixAccount
objectClass: top
<user data fields here>
memberOf: <DN for each group that this user belongs to>
```

Each group will have an entry like
Expand All @@ -61,6 +62,64 @@ objectClass: groupOfNames
objectClass: posixGroup
objectClass: top
<group data fields here>
member: <DN for each user belonging to this group>
```

## Primary groups

Note that each user will have an associated group to act as its POSIX user primary group

For example:

```ldif
dn: CN=sherlock.holmes,OU=users,DC=<your domain>
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: posixAccount
objectClass: top
...
memberOf: CN=sherlock.holmes,OU=groups,DC=<your domain>
...
```

will have an associated group

```ldif
dn: CN=sherlock.holmes,OU=groups,DC=<your domain>
objectClass: groupOfNames
objectClass: posixGroup
objectClass: top
...
member: CN=sherlock.holmes,OU=users,DC=<your domain>
...
```

## Mirrored groups

Each group of users will have an associated group-of-groups where each user in the group will have its user primary group in the group-of-groups.
Note that these groups-of-groups are **not** `posixGroup`s as POSIX does not allow nested groups.

For example:

```ldif
dn:CN=Detectives,OU=groups,DC=<your domain>
objectClass: groupOfNames
objectClass: posixGroup
objectClass: top
...
member: CN=sherlock.holmes,OU=users,DC=<your domain>
```

will have an associated group-of-groups

```ldif
dn: CN=Primary user groups for Detectives,OU=groups,DC=<your domain>
objectClass: groupOfNames
objectClass: top
...
member: CN=sherlock.holmes,OU=groups,DC=<your domain>
...
```

## OpenID Connect
Expand Down
3 changes: 1 addition & 2 deletions apricot/apricot_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ def __init__(
client_id=client_id,
client_secret=client_secret,
debug=debug,
domain=domain,
uid_cache=uid_cache,
**kwargs,
)
Expand All @@ -60,7 +59,7 @@ def __init__(
# Create an LDAPServerFactory
if self.debug:
log.msg("Creating an LDAPServerFactory.")
factory = OAuthLDAPServerFactory(oauth_client)
factory = OAuthLDAPServerFactory(domain, oauth_client)

# Attach a listening endpoint
if self.debug:
Expand Down
4 changes: 2 additions & 2 deletions apricot/ldap/oauth_ldap_server_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@


class OAuthLDAPServerFactory(ServerFactory):
def __init__(self, oauth_client: OAuthClient):
def __init__(self, domain: str, oauth_client: OAuthClient):
"""
Initialise an LDAPServerFactory
@param oauth_client: An OAuth client used to construct the LDAP tree
"""
# Create an LDAP lookup tree
self.adaptor = OAuthLDAPTree(oauth_client)
self.adaptor = OAuthLDAPTree(domain, oauth_client)

def __repr__(self) -> str:
return f"{self.__class__.__name__} using adaptor {self.adaptor}"
Expand Down
31 changes: 21 additions & 10 deletions apricot/ldap/oauth_ldap_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,26 @@
from zope.interface import implementer

from apricot.ldap.oauth_ldap_entry import OAuthLDAPEntry
from apricot.oauth import OAuthClient
from apricot.oauth import OAuthClient, OAuthDataAdaptor


@implementer(IConnectedLDAPEntry)
class OAuthLDAPTree:
oauth_client: OAuthClient

def __init__(self, oauth_client: OAuthClient, refresh_interval: int = 60) -> None:
def __init__(
self, domain: str, oauth_client: OAuthClient, refresh_interval: int = 60
) -> None:
"""
Initialise an OAuthLDAPTree
@param domain: The root domain of the LDAP tree
@param oauth_client: An OAuth client used to construct the LDAP tree
@param refresh_interval: Interval in seconds after which the tree must be refreshed
"""
self.debug = oauth_client.debug
self.domain = domain
self.last_update = time.monotonic()
self.oauth_client: OAuthClient = oauth_client
self.oauth_client = oauth_client
self.refresh_interval = refresh_interval
self.root_: OAuthLDAPEntry | None = None

Expand All @@ -42,30 +45,38 @@ def root(self) -> OAuthLDAPEntry:
not self.root_
or (time.monotonic() - self.last_update) > self.refresh_interval
):
log.msg("Rebuilding LDAP tree from OAuth data.")
# Update users and groups from the OAuth server
log.msg("Retrieving OAuth data.")
oauth_adaptor = OAuthDataAdaptor(self.domain, self.oauth_client)

# Create a root node for the tree
log.msg("Rebuilding LDAP tree.")
self.root_ = OAuthLDAPEntry(
dn=self.oauth_client.root_dn,
dn=oauth_adaptor.root_dn,
attributes={"objectClass": ["dcObject"]},
oauth_client=self.oauth_client,
)

# Add OUs for users and groups
groups_ou = self.root_.add_child(
"OU=groups", {"ou": ["groups"], "objectClass": ["organizationalUnit"]}
)
users_ou = self.root_.add_child(
"OU=users", {"ou": ["users"], "objectClass": ["organizationalUnit"]}
)

# Add groups to the groups OU
if self.debug:
log.msg("Adding groups to the LDAP tree.")
for group_attrs in self.oauth_client.validated_groups():
log.msg(f"Adding {len(oauth_adaptor.groups)} groups to the LDAP tree.")
for group_attrs in oauth_adaptor.groups:
groups_ou.add_child(f"CN={group_attrs.cn}", group_attrs.to_dict())

# Add users to the users OU
if self.debug:
log.msg("Adding users to the LDAP tree.")
for user_attrs in self.oauth_client.validated_users():
log.msg(f"Adding {len(oauth_adaptor.users)} users to the LDAP tree.")
for user_attrs in oauth_adaptor.users:
users_ou.add_child(f"CN={user_attrs.cn}", user_attrs.to_dict())

# Set last updated time
log.msg("Finished building LDAP tree.")
self.last_update = time.monotonic()
Expand Down
12 changes: 6 additions & 6 deletions apricot/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
from .ldap_attribute_adaptor import LDAPAttributeAdaptor
from .ldap_group_of_names import LDAPGroupOfNames
from .ldap_inetorgperson import LDAPInetOrgPerson
from .ldap_inetuser import LDAPInetUser
from .ldap_oauthuser import LDAPOAuthUser
from .ldap_person import LDAPPerson
from .ldap_posix_account import LDAPPosixAccount
from .ldap_posix_group import LDAPPosixGroup
from .named_ldap_class import NamedLDAPClass
from .overlay_memberof import OverlayMemberOf
from .overlay_oauthentry import OverlayOAuthEntry

__all__ = [
"LDAPAttributeAdaptor",
"LDAPGroupOfNames",
"LDAPInetOrgPerson",
"LDAPInetUser",
"LDAPOAuthUser",
"LDAPPerson",
"LDAPPosixAccount",
"LDAPPosixGroup",
"NamedLDAPClass",
"OverlayMemberOf",
"OverlayOAuthEntry",
]
16 changes: 14 additions & 2 deletions apricot/models/ldap_group_of_names.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
from pydantic import BaseModel
from .named_ldap_class import NamedLDAPClass


class LDAPGroupOfNames(BaseModel):
class LDAPGroupOfNames(NamedLDAPClass):
"""
A group with named members
OID: 2.5.6.9
Object class: Structural
Parent: top
Schema: rfc4519
"""

cn: str
description: str
member: list[str]

def names(self) -> list[str]:
return ["groupOfNames"]
17 changes: 14 additions & 3 deletions apricot/models/ldap_inetorgperson.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
from pydantic import BaseModel
from .ldap_organizational_person import LDAPOrganizationalPerson


class LDAPInetOrgPerson(BaseModel):
class LDAPInetOrgPerson(LDAPOrganizationalPerson):
"""
A person belonging to an internet/intranet directory service
OID: 2.16.840.1.113730.3.2.2
Object class: Structural
Parent: organizationalPerson
Schema: rfc2798
"""

cn: str
description: str
displayName: str # noqa: N815
givenName: str # noqa: N815
sn: str

def names(self) -> list[str]:
return [*super().names(), "inetOrgPerson"]
6 changes: 0 additions & 6 deletions apricot/models/ldap_inetuser.py

This file was deleted.

5 changes: 0 additions & 5 deletions apricot/models/ldap_oauthuser.py

This file was deleted.

17 changes: 17 additions & 0 deletions apricot/models/ldap_organizational_person.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from .ldap_person import LDAPPerson


class LDAPOrganizationalPerson(LDAPPerson):
"""
A person belonging to an organisation
OID: 2.5.6.7
Object class: Structural
Parent: person
Schema: rfc4519
"""

description: str

def names(self) -> list[str]:
return [*super().names(), "organizationalPerson"]
16 changes: 14 additions & 2 deletions apricot/models/ldap_person.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
from pydantic import BaseModel
from .named_ldap_class import NamedLDAPClass


class LDAPPerson(BaseModel):
class LDAPPerson(NamedLDAPClass):
"""
A named person
OID: 2.5.6.6
Object class: Structural
Parent: top
Schema: rfc4519
"""

cn: str
sn: str

def names(self) -> list[str]:
return ["person"]
18 changes: 16 additions & 2 deletions apricot/models/ldap_posix_account.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import re

from pydantic import BaseModel, StringConstraints, validator
from pydantic import StringConstraints, validator
from typing_extensions import Annotated

from .named_ldap_class import NamedLDAPClass

ID_MIN = 2000
ID_MAX = 60000


class LDAPPosixAccount(BaseModel):
class LDAPPosixAccount(NamedLDAPClass):
"""
Abstraction of an account with POSIX attributes
OID: 1.3.6.1.1.1.2.0
Object class: Auxiliary
Parent: top
Schema: rfc2307bis
"""

cn: str
gidNumber: int # noqa: N815
homeDirectory: Annotated[ # noqa: N815
Expand Down Expand Up @@ -38,3 +49,6 @@ def validate_uid_number(cls, uid_number: int) -> int:
msg = f"Must be in range {ID_MIN} to {ID_MAX}."
raise ValueError(msg)
return uid_number

def names(self) -> list[str]:
return ["posixAccount"]
18 changes: 16 additions & 2 deletions apricot/models/ldap_posix_group.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
from pydantic import BaseModel, validator
from pydantic import validator

from .named_ldap_class import NamedLDAPClass

ID_MIN = 2000
ID_MAX = 4294967295


class LDAPPosixGroup(BaseModel):
class LDAPPosixGroup(NamedLDAPClass):
"""
Abstraction of a group of accounts
OID: 1.3.6.1.1.1.2.2
Object class: Auxiliary
Parent: top
Schema: rfc2307bis
"""

description: str
gidNumber: int # noqa: N815
memberUid: list[str] # noqa: N815
Expand All @@ -17,3 +28,6 @@ def validate_gid_number(cls, gid_number: int) -> int:
msg = f"Must be in range {ID_MIN} to {ID_MAX}."
raise ValueError(msg)
return gid_number

def names(self) -> list[str]:
return ["posixGroup"]
7 changes: 7 additions & 0 deletions apricot/models/named_ldap_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pydantic import BaseModel


class NamedLDAPClass(BaseModel):
def names(self) -> list[str]:
"""List of names for this LDAP object class"""
return []
Loading

0 comments on commit 83c1bfa

Please sign in to comment.