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

Validate LDAP data #18

Merged
merged 6 commits into from
Feb 23, 2024
Merged
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
7 changes: 4 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ WORKDIR /app
RUN apk add --update --no-cache \
gcc libc-dev libffi-dev

# Upload and install Python package and dependencies
COPY ./apricot apricot
# Install Python dependencies
COPY ./pyproject.toml .
COPY ./README.md .
RUN pip install --upgrade hatch pip
# Copy code and README into the container
COPY ./README.md .
COPY ./apricot apricot
# Initialise environment with hatch
RUN hatch run true

Expand Down
6 changes: 0 additions & 6 deletions apricot/ldap/oauth_ldap_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,6 @@ def oauth_client(self) -> OAuthClient:
raise TypeError(msg)
return self.oauth_client_

@property
def username(self) -> str:
username = self.dn.split()[0].getText().split("CN=")[1]
domain = self.dn.getDomainName()
return f"{username}@{domain}"

def add_child(
self, rdn: RelativeDistinguishedName | str, attributes: LDAPAttributeDict
) -> "OAuthLDAPEntry":
Expand Down
8 changes: 4 additions & 4 deletions apricot/ldap/oauth_ldap_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ def __init__(self, oauth_client: OAuthClient) -> None:
"OU=users", {"ou": ["users"], "objectClass": ["organizationalUnit"]}
)
# Add groups to the groups OU
for group_attrs in self.oauth_client.groups():
groups_ou.add_child(f"CN={group_attrs['name'][0]}", group_attrs)
for group_attrs in self.oauth_client.validated_groups():
groups_ou.add_child(f"CN={group_attrs['cn'][0]}", group_attrs)
# Add users to the users OU
for user_attrs in self.oauth_client.users():
users_ou.add_child(f"CN={user_attrs['name'][0]}", user_attrs)
for user_attrs in self.oauth_client.validated_users():
users_ou.add_child(f"CN={user_attrs['cn'][0]}", user_attrs)

def __repr__(self) -> str:
return f"{self.__class__.__name__} with backend {self.oauth_client.__class__.__name__}"
Expand Down
40 changes: 30 additions & 10 deletions apricot/ldap/read_only_ldap_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ def getRootDSE( # noqa: N802
request: LDAPBindRequest,
reply: Callable[[LDAPSearchResultEntry], None] | None,
) -> LDAPSearchResultDone:
"""Handle an LDAP Root RSE request"""
"""
Handle an LDAP Root RSE request
"""
return super().getRootDSE(request, reply)

def handle_LDAPAddRequest( # noqa: N802
Expand All @@ -27,7 +29,9 @@ def handle_LDAPAddRequest( # noqa: N802
controls: LDAPControl | None,
reply: Callable[..., None] | None,
) -> defer.Deferred[ILDAPEntry]:
"""Refuse to handle an LDAP add request"""
"""
Refuse to handle an LDAP add request
"""
id((request, controls, reply)) # ignore unused arguments
msg = "ReadOnlyLDAPServer will not handle LDAP add requests"
raise LDAPProtocolError(msg)
Expand All @@ -38,7 +42,9 @@ def handle_LDAPBindRequest( # noqa: N802
controls: LDAPControl | None,
reply: Callable[..., None] | None,
) -> defer.Deferred[ILDAPEntry]:
"""Handle an LDAP bind request"""
"""
Handle an LDAP bind request
"""
return super().handle_LDAPBindRequest(request, controls, reply)

def handle_LDAPCompareRequest( # noqa: N802
Expand All @@ -47,7 +53,9 @@ def handle_LDAPCompareRequest( # noqa: N802
controls: LDAPControl | None,
reply: Callable[..., None] | None,
) -> defer.Deferred[ILDAPEntry]:
"""Handle an LDAP compare request"""
"""
Handle an LDAP compare request
"""
return super().handle_LDAPCompareRequest(request, controls, reply)

def handle_LDAPDelRequest( # noqa: N802
Expand All @@ -56,7 +64,9 @@ def handle_LDAPDelRequest( # noqa: N802
controls: LDAPControl | None,
reply: Callable[..., None] | None,
) -> defer.Deferred[ILDAPEntry]:
"""Refuse to handle an LDAP delete request"""
"""
Refuse to handle an LDAP delete request
"""
id((request, controls, reply)) # ignore unused arguments
msg = "ReadOnlyLDAPServer will not handle LDAP delete requests"
raise LDAPProtocolError(msg)
Expand All @@ -67,7 +77,9 @@ def handle_LDAPExtendedRequest( # noqa: N802
controls: LDAPControl | None,
reply: Callable[..., None] | None,
) -> defer.Deferred[ILDAPEntry]:
"""Handle an LDAP extended request"""
"""
Handle an LDAP extended request
"""
return super().handle_LDAPExtendedRequest(request, controls, reply)

def handle_LDAPModifyDNRequest( # noqa: N802
Expand All @@ -76,7 +88,9 @@ def handle_LDAPModifyDNRequest( # noqa: N802
controls: LDAPControl | None,
reply: Callable[..., None] | None,
) -> defer.Deferred[ILDAPEntry]:
"""Refuse to handle an LDAP modify DN request"""
"""
Refuse to handle an LDAP modify DN request
"""
id((request, controls, reply)) # ignore unused arguments
msg = "ReadOnlyLDAPServer will not handle LDAP modify DN requests"
raise LDAPProtocolError(msg)
Expand All @@ -87,7 +101,9 @@ def handle_LDAPModifyRequest( # noqa: N802
controls: LDAPControl | None,
reply: Callable[..., None] | None,
) -> defer.Deferred[ILDAPEntry]:
"""Refuse to handle an LDAP modify request"""
"""
Refuse to handle an LDAP modify request
"""
id((request, controls, reply)) # ignore unused arguments
msg = "ReadOnlyLDAPServer will not handle LDAP modify requests"
raise LDAPProtocolError(msg)
Expand All @@ -98,7 +114,9 @@ def handle_LDAPUnbindRequest( # noqa: N802
controls: LDAPControl | None,
reply: Callable[..., None] | None,
) -> None:
"""Handle an LDAP unbind request"""
"""
Handle an LDAP unbind request
"""
super().handle_LDAPUnbindRequest(request, controls, reply)

def handle_LDAPSearchRequest( # noqa: N802
Expand All @@ -107,5 +125,7 @@ def handle_LDAPSearchRequest( # noqa: N802
controls: LDAPControl | None,
reply: Callable[[LDAPSearchResultEntry], None] | None,
) -> defer.Deferred[ILDAPEntry]:
"""Handle an LDAP search request"""
"""
Handle an LDAP search request
"""
return super().handle_LDAPSearchRequest(request, controls, reply)
15 changes: 15 additions & 0 deletions apricot/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from .ldap_group_of_names import LdapGroupOfNames
from .ldap_inetorgperson import LdapInetOrgPerson
from .ldap_inetuser import LdapInetUser
from .ldap_person import LdapPerson
from .ldap_posix_account import LdapPosixAccount
from .ldap_posix_group import LdapPosixGroup

__all__ = [
"LdapGroupOfNames",
"LdapInetOrgPerson",
"LdapInetUser",
"LdapPerson",
"LdapPosixAccount",
"LdapPosixGroup",
]
7 changes: 7 additions & 0 deletions apricot/models/ldap_group_of_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pydantic import BaseModel


class LdapGroupOfNames(BaseModel):
cn: str
description: str
member: list[str]
9 changes: 9 additions & 0 deletions apricot/models/ldap_inetorgperson.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from pydantic import BaseModel


class LdapInetOrgPerson(BaseModel):
cn: str
description: str
displayName: str # noqa: N815
givenName: str # noqa: N815
sn: str
6 changes: 6 additions & 0 deletions apricot/models/ldap_inetuser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel


class LdapInetUser(BaseModel):
memberOf: list[str] # noqa: N815
uid: str
6 changes: 6 additions & 0 deletions apricot/models/ldap_person.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel


class LdapPerson(BaseModel):
cn: str
sn: str
40 changes: 40 additions & 0 deletions apricot/models/ldap_posix_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import re

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

ID_MIN = 2000
ID_MAX = 60000


class LdapPosixAccount(BaseModel):
cn: str
gidNumber: int # noqa: N815
homeDirectory: Annotated[ # noqa: N815
str, StringConstraints(strip_whitespace=True, to_lower=True)
]
uid: str
uidNumber: int # noqa: N815

@validator("gidNumber") # type: ignore[misc]
@classmethod
def validate_gid_number(cls, gid_number: int) -> int:
"""Avoid conflicts with existing users"""
if not ID_MIN <= gid_number <= ID_MAX:
msg = f"Must be in range {ID_MIN} to {ID_MAX}."
raise ValueError(msg)
return gid_number

@validator("homeDirectory") # type: ignore[misc]
@classmethod
def validate_home_directory(cls, home_directory: str) -> str:
return re.sub(r"\s+", "-", home_directory)

@validator("uidNumber") # type: ignore[misc]
@classmethod
def validate_uid_number(cls, uid_number: int) -> int:
"""Avoid conflicts with existing users"""
if not ID_MIN <= uid_number <= ID_MAX:
msg = f"Must be in range {ID_MIN} to {ID_MAX}."
raise ValueError(msg)
return uid_number
19 changes: 19 additions & 0 deletions apricot/models/ldap_posix_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from pydantic import BaseModel, validator

ID_MIN = 2000
ID_MAX = 4294967295


class LdapPosixGroup(BaseModel):
description: str
gidNumber: int # noqa: N815
memberUid: list[str] # noqa: N815

@validator("gidNumber") # type: ignore[misc]
@classmethod
def validate_gid_number(cls, gid_number: int) -> int:
"""Avoid conflicts with existing groups"""
if not ID_MIN <= gid_number <= ID_MAX:
msg = f"Must be in range {ID_MIN} to {ID_MAX}."
raise ValueError(msg)
return gid_number
62 changes: 40 additions & 22 deletions apricot/oauth/microsoft_entra_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Any
from typing import Any, cast

from .oauth_client import OAuthClient
from .types import JSONDict, LDAPAttributeDict
from .types import JSONDict


class MicrosoftEntraClient(OAuthClient):
Expand All @@ -25,42 +25,65 @@ def __init__(
def extract_token(self, json_response: JSONDict) -> str:
return str(json_response["access_token"])

def groups(self) -> list[LDAPAttributeDict]:
def groups(self) -> list[dict[str, Any]]:
output = []
try:
group_data = self.query("https://graph.microsoft.com/v1.0/groups/")
for group_dict in group_data["value"]:
attributes = {k: [v if v else ""] for k, v in dict(group_dict).items()}
attributes["objectclass"] = ["top", "group"]
attributes["name"] = [str(group_dict["displayName"]).split("@")[0]]
for group_dict in cast(list[dict[str, Any]], group_data["value"]):
attributes = {}
attributes["cn"] = group_dict.get("displayName", None)
attributes["description"] = group_dict.get("id", None)
# As we cannot manually set any attributes we take the last part of the securityIdentifier
attributes["gidNumber"] = str(
group_dict.get("securityIdentifier", "")
).split("-")[-1]
# Add membership attributes
members = self.query(
f"https://graph.microsoft.com/v1.0/groups/{group_dict['id']}/members"
)
attributes["memberUid"] = [
str(user["userPrincipalName"]).split("@")[0]
for user in members["value"]
if user["userPrincipalName"]
]
attributes["member"] = [
f"CN={uid},OU=users,{self.root_dn}"
for uid in attributes["memberUid"]
]
output.append(attributes)
except KeyError:
pass
return output

def users(self) -> list[LDAPAttributeDict]:
def users(self) -> list[dict[str, Any]]:
output = []
try:
queries = [
"displayName",
"givenName",
"id",
"mail",
"surname",
"userPrincipalName",
self.uid_attribute,
]
user_data = self.query(
f"https://graph.microsoft.com/v1.0/users?$select={','.join(queries)}"
)
for user_dict in user_data["value"]:
attributes = {k: [v if v else ""] for k, v in dict(user_dict).items()}
attributes["objectclass"] = [
"top",
"person",
"organizationalPerson",
"user",
]
for user_dict in cast(list[dict[str, Any]], user_data["value"]):
# Get user attributes
uid, domain = str(user_dict.get("userPrincipalName", "@")).split("@")
attributes = {}
attributes["cn"] = user_dict.get("displayName", None)
attributes["description"] = user_dict.get("id", None)
attributes["displayName"] = attributes.get("cn", None)
attributes["domain"] = domain
attributes["gidNumber"] = user_dict.get(self.uid_attribute, None)
attributes["givenName"] = user_dict.get("givenName", "")
attributes["homeDirectory"] = f"/home/{uid}" if uid else None
attributes["sn"] = user_dict.get("surname", "")
attributes["uid"] = uid if uid else None
attributes["uidNumber"] = user_dict.get(self.uid_attribute, None)
# Add group attributes
group_memberships = self.query(
f"https://graph.microsoft.com/v1.0/users/{user_dict['id']}/memberOf"
)
Expand All @@ -69,11 +92,6 @@ def users(self) -> list[LDAPAttributeDict]:
for group in group_memberships["value"]
if group["displayName"]
]
attributes["name"] = [str(user_dict["userPrincipalName"]).split("@")[0]]
attributes["domain"] = [
str(user_dict["userPrincipalName"]).split("@")[1]
]
attributes["uid"] = [str(user_dict[self.uid_attribute])]
output.append(attributes)
except KeyError:
pass
Expand Down
Loading
Loading