Added initial container definition for uffd-ldapd
Signed-off-by: Nis Wechselberg <enbewe@enbewe.de>
This commit is contained in:
parent
f36cc07b65
commit
27d78f4719
4 changed files with 402 additions and 0 deletions
21
Containerfile
Normal file
21
Containerfile
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
FROM docker.io/library/debian:bookworm-20250520-slim
|
||||||
|
|
||||||
|
RUN apt-get -qq update && \
|
||||||
|
apt-get -qq dist-upgrade && \
|
||||||
|
apt-get -qq install ca-certificates
|
||||||
|
|
||||||
|
# Add key and config for cccv repository
|
||||||
|
COPY cccv-archive-key.asc /etc/apt/trusted.gpg.d/
|
||||||
|
COPY cccv-archive.list /etc/apt/sources.list.d/
|
||||||
|
|
||||||
|
# Install uffd-ldapd from (new) package sources
|
||||||
|
RUN apt-get -qq update && \
|
||||||
|
apt-get -qq install uffd-ldapd
|
||||||
|
|
||||||
|
# Copy the patched version of uffd-ldapd (for now) to allow SIGTERM handling
|
||||||
|
COPY uffd-ldapd /usr/bin/uffd-ldapd
|
||||||
|
|
||||||
|
EXPOSE 389/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/uffd-ldapd"]
|
||||||
|
CMD ["--socket-address", "0.0.0.0"]
|
41
cccv-archive-key.asc
Normal file
41
cccv-archive-key.asc
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
JRWMzY9EoBOHB1PubZuzUNIUQeui1vyX
|
||||||
|
=uRc5
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
1
cccv-archive.list
Normal file
1
cccv-archive.list
Normal file
|
@ -0,0 +1 @@
|
||||||
|
deb [signed-by=/etc/apt/trusted.gpg.d/cccv-archive-key.asc] https://packages.cccv.de/uffd bookworm main
|
339
uffd-ldapd
Executable file
339
uffd-ldapd
Executable file
|
@ -0,0 +1,339 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import socketserver
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
|
||||||
|
import click
|
||||||
|
import requests
|
||||||
|
from cachecontrol import CacheControl
|
||||||
|
from cachecontrol.heuristics import ExpiresAfter
|
||||||
|
|
||||||
|
import ldapserver
|
||||||
|
from ldapserver.exceptions import LDAPInvalidCredentials, LDAPInsufficientAccessRights, LDAPUnwillingToPerform
|
||||||
|
from ldapserver.schema import RFC2307BIS_SCHEMA, RFC2798_SCHEMA
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CUSTOM_SCHEMA = (RFC2307BIS_SCHEMA|RFC2798_SCHEMA).extend(attribute_type_definitions=[
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
"( 1.2.840.113556.1.2.102 NAME 'memberOf' DESC 'Group that the entry belongs to' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 USAGE dSAOperation )"
|
||||||
|
])
|
||||||
|
|
||||||
|
class UffdAPI:
|
||||||
|
def __init__(self, baseurl, client_id, client_secret, cache_ttl=60):
|
||||||
|
self.baseurl = baseurl
|
||||||
|
self.client_id = client_id
|
||||||
|
self.client_secret = client_secret
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.auth = (client_id, client_secret)
|
||||||
|
if cache_ttl:
|
||||||
|
self.session = CacheControl(self.session, heuristic=ExpiresAfter(seconds=cache_ttl))
|
||||||
|
|
||||||
|
def get(self, endpoint, **kwargs):
|
||||||
|
resp = self.session.get(self.baseurl + endpoint, params=kwargs)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
def post(self, endpoint, **kwargs):
|
||||||
|
resp = self.session.post(self.baseurl + endpoint, data=kwargs)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name,redefined-builtin
|
||||||
|
def get_users(self, id=None, loginname=None, group=None):
|
||||||
|
return self.get('/api/v1/getusers', id=id, loginname=loginname, group=group)
|
||||||
|
|
||||||
|
def get_groups(self, id=None, name=None, member=None):
|
||||||
|
return self.get('/api/v1/getgroups', id=id, name=name, member=member)
|
||||||
|
|
||||||
|
def check_password(self, loginname, password):
|
||||||
|
return self.post('/api/v1/checkpassword', loginname=loginname, password=password)
|
||||||
|
|
||||||
|
def normalize_user_loginname(loginname):
|
||||||
|
# The equality matching rule for uid is caseIgnoreMatch. It prepares
|
||||||
|
# attribute and assertion value according to LDAP stringprep with
|
||||||
|
# case-folding.
|
||||||
|
#
|
||||||
|
# Uffd restricts loginnames to lower-case ASCII letters, digits,
|
||||||
|
# underscores and dashes. None of these characters are changed or
|
||||||
|
# rejetced by stringprep with case-folding. The effect stringprep has
|
||||||
|
# on loginnames is that it adds a leading and a final SPACE character.
|
||||||
|
#
|
||||||
|
# The assertion value (the argument to this function) could however contain
|
||||||
|
# characters that are mapped to SPACE or nothing for example. So we apply
|
||||||
|
# stringprep to the assertion value and then strip the added leading and
|
||||||
|
# final SPACE characters. Stringprep case-folds the input string to
|
||||||
|
# lower-case.
|
||||||
|
#
|
||||||
|
# The resulting string can be compared to an actual loginname with simple
|
||||||
|
# byte-for-byte or codepoint-for-codepoint comparison with or without
|
||||||
|
# case-folding. It matches if and only if the loginname matches the input
|
||||||
|
# value according to caseIgnoreMatch.
|
||||||
|
try:
|
||||||
|
return ldapserver.rfc4518_stringprep.prepare(loginname, ldapserver.rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING).strip(' ')
|
||||||
|
except ValueError: # Input value contains prohibited characters
|
||||||
|
return None
|
||||||
|
|
||||||
|
def normalize_group_name(name):
|
||||||
|
# Currently uffd has no restrictions for group names, but it is planned
|
||||||
|
# to add restrictions similar to loginname restrictions.
|
||||||
|
# See https://git.cccv.de/uffd/uffd/-/issues/127
|
||||||
|
return normalize_user_loginname(name)
|
||||||
|
|
||||||
|
class UffdLDAPRequestHandler(ldapserver.LDAPRequestHandler):
|
||||||
|
subschema = ldapserver.SubschemaSubentry(CUSTOM_SCHEMA, 'cn=Subschema')
|
||||||
|
|
||||||
|
# Overwritten before use
|
||||||
|
api = None
|
||||||
|
dn_base = None
|
||||||
|
bind_password = None # if None anonymous reads are allowed
|
||||||
|
group_filter_regex = None
|
||||||
|
|
||||||
|
def do_bind_simple_authenticated(self, dn, password):
|
||||||
|
dn = self.subschema.DN.from_str(dn)
|
||||||
|
if dn == self.subschema.DN('cn=service,ou=system') + self.dn_base and password == self.bind_password:
|
||||||
|
return True
|
||||||
|
if not dn.is_direct_child_of(self.subschema.DN('ou=users') + self.dn_base) or len(dn[0]) != 1 or dn[0][0].attribute != 'uid':
|
||||||
|
raise LDAPInvalidCredentials()
|
||||||
|
try:
|
||||||
|
if self.api.check_password(loginname=dn[0][0].value, password=password):
|
||||||
|
return True
|
||||||
|
except requests.exceptions.HTTPError as exc:
|
||||||
|
if exc.response.status_code == 403: # We don't have "checkpassword" scope
|
||||||
|
raise LDAPInsufficientAccessRights() from exc
|
||||||
|
if exc.response.status_code == 429: # Ratelimited
|
||||||
|
raise LDAPUnwillingToPerform('Too Many Requests') from exc
|
||||||
|
raise exc
|
||||||
|
raise LDAPInvalidCredentials()
|
||||||
|
|
||||||
|
supports_sasl_plain = True
|
||||||
|
|
||||||
|
def do_bind_sasl_plain(self, identity, password, authzid=None):
|
||||||
|
if authzid is not None and identity != authzid:
|
||||||
|
raise LDAPInvalidCredentials()
|
||||||
|
try:
|
||||||
|
if self.api.check_password(loginname=identity, password=password):
|
||||||
|
return True
|
||||||
|
except requests.exceptions.HTTPError as exc:
|
||||||
|
if exc.response.status_code == 403: # We don't have "checkpassword" scope
|
||||||
|
raise LDAPInsufficientAccessRights() from exc
|
||||||
|
if exc.response.status_code == 429: # Ratelimited
|
||||||
|
raise LDAPUnwillingToPerform('Too Many Requests') from exc
|
||||||
|
raise exc
|
||||||
|
raise LDAPInvalidCredentials()
|
||||||
|
|
||||||
|
def do_search(self, baseobj, scope, filterobj):
|
||||||
|
yield from super().do_search(baseobj, scope, filterobj)
|
||||||
|
if self.bind_object or self.bind_password is None:
|
||||||
|
yield from self.do_search_static()
|
||||||
|
yield from self.do_search_users(baseobj, scope, filterobj)
|
||||||
|
yield from self.do_search_groups(baseobj, scope, filterobj)
|
||||||
|
|
||||||
|
def do_search_static(self):
|
||||||
|
base_attrs = {
|
||||||
|
'objectClass': ['top', 'dcObject', 'organization'],
|
||||||
|
'structuralObjectClass': ['organization'],
|
||||||
|
}
|
||||||
|
for rdnassertion in self.dn_base[0]: # pylint: disable=unsubscriptable-object
|
||||||
|
base_attrs[rdnassertion.attribute] = [rdnassertion.value]
|
||||||
|
yield self.subschema.ObjectEntry(self.dn_base, **base_attrs)
|
||||||
|
yield self.subschema.ObjectEntry(self.subschema.DN('ou=users') + self.dn_base,
|
||||||
|
ou=['users'],
|
||||||
|
objectClass=['top', 'organizationalUnit'],
|
||||||
|
structuralObjectClass=['organizationalUnit'],
|
||||||
|
)
|
||||||
|
yield self.subschema.ObjectEntry(self.subschema.DN('ou=groups') + self.dn_base,
|
||||||
|
ou=['groups'],
|
||||||
|
objectClass=['top', 'organizationalUnit'],
|
||||||
|
structuralObjectClass=['organizationalUnit'],
|
||||||
|
)
|
||||||
|
yield self.subschema.ObjectEntry(self.subschema.DN('ou=system') + self.dn_base,
|
||||||
|
ou=['system'],
|
||||||
|
objectClass=['top', 'organizationalUnit'],
|
||||||
|
structuralObjectClass=['organizationalUnit'],
|
||||||
|
)
|
||||||
|
yield self.subschema.ObjectEntry(self.subschema.DN('cn=service,ou=system') + self.dn_base,
|
||||||
|
cn=['service'],
|
||||||
|
objectClass=['top', 'organizationalRole', 'simpleSecurityObject'],
|
||||||
|
structuralObjectClass=['organizationalRole'],
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_search_users(self, baseobj, scope, filterobj):
|
||||||
|
template = self.subschema.EntryTemplate(self.subschema.DN(self.dn_base, ou='users'), 'uid',
|
||||||
|
structuralObjectClass=['inetorgperson'],
|
||||||
|
objectClass=['top', 'inetorgperson', 'organizationalperson', 'person', 'posixaccount'],
|
||||||
|
cn=ldapserver.WILDCARD,
|
||||||
|
displayname=ldapserver.WILDCARD,
|
||||||
|
givenname=ldapserver.WILDCARD,
|
||||||
|
homeDirectory=ldapserver.WILDCARD,
|
||||||
|
mail=ldapserver.WILDCARD,
|
||||||
|
sn=[' '],
|
||||||
|
uid=ldapserver.WILDCARD,
|
||||||
|
uidNumber=ldapserver.WILDCARD,
|
||||||
|
memberOf=ldapserver.WILDCARD,
|
||||||
|
)
|
||||||
|
if not template.match_search(baseobj, scope, filterobj):
|
||||||
|
return
|
||||||
|
constraints = template.extract_search_constraints(baseobj, scope, filterobj)
|
||||||
|
request_params = {}
|
||||||
|
if 'uid' in constraints:
|
||||||
|
request_params = {'loginname': normalize_user_loginname(constraints['uid'][0])}
|
||||||
|
elif 'uidnumber' in constraints:
|
||||||
|
request_params = {'id': constraints['uidnumber'][0]}
|
||||||
|
elif 'memberof' in constraints:
|
||||||
|
for value in constraints['memberof']:
|
||||||
|
if value.is_direct_child_of(self.subschema.DN(self.dn_base, ou='groups')) and value.object_attribute == 'cn':
|
||||||
|
request_params = {'group': normalize_group_name(value.object_value)}
|
||||||
|
break
|
||||||
|
if 'group' in request_params and not self.group_filter_regex.match(request_params['group']):
|
||||||
|
return
|
||||||
|
for user in self.api.get_users(**request_params):
|
||||||
|
yield template.create_entry(user['loginname'],
|
||||||
|
cn=[user['displayname']],
|
||||||
|
displayname=[user['displayname']],
|
||||||
|
givenname=[user['displayname']],
|
||||||
|
homeDirectory=['/home/'+user['loginname']],
|
||||||
|
mail=[user['email']],
|
||||||
|
uid=[user['loginname']],
|
||||||
|
uidNumber=[user['id']],
|
||||||
|
memberOf=[self.subschema.DN(self.subschema.DN(self.dn_base, ou='groups'), cn=group)
|
||||||
|
for group in user['groups']
|
||||||
|
if self.group_filter_regex.match(group)],
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_search_groups(self, baseobj, scope, filterobj):
|
||||||
|
template = self.subschema.EntryTemplate(self.subschema.DN(self.dn_base, ou='groups'), 'cn',
|
||||||
|
structuralObjectClass=['groupOfUniqueNames'],
|
||||||
|
objectClass=['top', 'groupOfUniqueNames', 'posixGroup'],
|
||||||
|
cn=ldapserver.WILDCARD,
|
||||||
|
description=[' '],
|
||||||
|
gidNumber=ldapserver.WILDCARD,
|
||||||
|
uniqueMember=ldapserver.WILDCARD,
|
||||||
|
)
|
||||||
|
if not template.match_search(baseobj, scope, filterobj):
|
||||||
|
return
|
||||||
|
constraints = template.extract_search_constraints(baseobj, scope, filterobj)
|
||||||
|
request_params = {}
|
||||||
|
if 'cn' in constraints:
|
||||||
|
request_params = {'name': normalize_group_name(constraints['cn'][0])}
|
||||||
|
elif 'gidnumber' in constraints:
|
||||||
|
request_params = {'id': constraints['gidnumber'][0]}
|
||||||
|
elif 'uniquemember' in constraints:
|
||||||
|
for value in constraints['uniquemember']:
|
||||||
|
if value.is_direct_child_of(self.subschema.DN(self.dn_base, ou='users')) and value.object_attribute == 'uid':
|
||||||
|
request_params = {'member': normalize_user_loginname(value.object_value)}
|
||||||
|
break
|
||||||
|
if 'name' in request_params and not self.group_filter_regex.match(request_params['name']):
|
||||||
|
return
|
||||||
|
for group in self.api.get_groups(**request_params):
|
||||||
|
if not self.group_filter_regex.match(group['name']):
|
||||||
|
continue
|
||||||
|
yield template.create_entry(group['name'],
|
||||||
|
cn=[group['name']],
|
||||||
|
gidNumber=[group['id']],
|
||||||
|
uniqueMember=[self.subschema.DN(self.subschema.DN(self.dn_base, ou='users'), uid=user) for user in group['members']],
|
||||||
|
)
|
||||||
|
|
||||||
|
def make_requesthandler(api, dn_base, bind_password=None, group_filter_regex=None):
|
||||||
|
class RequestHandler(UffdLDAPRequestHandler):
|
||||||
|
pass
|
||||||
|
dn_base = RequestHandler.subschema.DN.from_str(dn_base)
|
||||||
|
RequestHandler.api = api
|
||||||
|
RequestHandler.dn_base = dn_base
|
||||||
|
RequestHandler.bind_password = bind_password.encode() if bind_password else None
|
||||||
|
RequestHandler.group_filter_regex = re.compile(group_filter_regex) if group_filter_regex else re.compile('')
|
||||||
|
return RequestHandler
|
||||||
|
|
||||||
|
class FilenoUnixStreamServer(socketserver.UnixStreamServer):
|
||||||
|
def __init__(self, fd, RequestHandlerClass, bind_and_activate=True):
|
||||||
|
self.server_fd = fd
|
||||||
|
super().__init__(None, RequestHandlerClass, bind_and_activate=bind_and_activate)
|
||||||
|
|
||||||
|
def server_bind(self):
|
||||||
|
self.socket.close() # UnixStreamServer.__init__ creates an unbound socket
|
||||||
|
self.socket = socket.fromfd(self.server_fd, socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
self.server_address = self.socket.getsockname()
|
||||||
|
|
||||||
|
class ThreadingFilenoUnixStreamServer(socketserver.ThreadingMixIn, FilenoUnixStreamServer):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cleanup_unix_socket(path):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return
|
||||||
|
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
try:
|
||||||
|
conn.connect(path)
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
os.remove(path)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def parse_network_address(addr):
|
||||||
|
port = '389'
|
||||||
|
if addr.startswith('['):
|
||||||
|
addr, remainder = addr[1:].split(']', 1)
|
||||||
|
if remainder.startswith(':'):
|
||||||
|
port = remainder[1:]
|
||||||
|
elif ':' in addr:
|
||||||
|
addr, port = addr.split(':')
|
||||||
|
return addr, port
|
||||||
|
|
||||||
|
class StdoutFilter(logging.Filter):
|
||||||
|
def filter(self, record):
|
||||||
|
return record.levelno <= logging.INFO
|
||||||
|
|
||||||
|
def sigterm_handler(signum, frame):
|
||||||
|
logger.info("Received SIGTERM, shutting down gracefully ...")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
@click.command(help='LDAP proxy for integrating LDAP service with uffd SSO. Supports user and group searches and as well as binds with user passwords.')
|
||||||
|
@click.option('--socket-address', help='Host and port "ip:port" to listen on')
|
||||||
|
@click.option('--socket-path', type=click.Path(), help='Path for UNIX domain socket')
|
||||||
|
@click.option('--socket-fd', type=int, help='Use fd number as server socket (alternative to --socket-path)')
|
||||||
|
@click.option('--api-url', required=True, help='Uffd base URL without API prefix or trailing slash (e.g. https://example.com)')
|
||||||
|
@click.option('--api-user', required=True, help='API user/client id')
|
||||||
|
@click.option('--api-secret', required=True, help='API secret, do not set this on the command-line, use environment variable SERVER_API_SECRET instead')
|
||||||
|
@click.option('--cache-ttl', default=60, help='Time-to-live for API response caching in seconds')
|
||||||
|
@click.option('--base-dn', required=True, help='Base DN for user, group and system objects. E.g. "dc=example,dc=com"')
|
||||||
|
@click.option('--bind-password', help='Authentication password for the service connection to LDAP. Bind DN is always "cn=service,ou=system,BASEDN". If set, anonymous access is disabled.')
|
||||||
|
@click.option('--group-filter-regex', help='Python regular expression that group names must match for the group to be visible to LDAP clients')
|
||||||
|
def main(socket_address, socket_path, socket_fd, api_url, api_user, api_secret, cache_ttl, base_dn, bind_password, group_filter_regex):
|
||||||
|
# Register signal handler for proper SIGTERM handling
|
||||||
|
signal.signal(signal.SIGTERM, sigterm_handler)
|
||||||
|
|
||||||
|
# pylint: disable=too-many-locals
|
||||||
|
if (socket_address is not None) \
|
||||||
|
+ (socket_path is not None) \
|
||||||
|
+ (socket_fd is not None) != 1:
|
||||||
|
raise click.ClickException('Either --socket-address, --socket-path or --socket-fd must be specified')
|
||||||
|
|
||||||
|
stdout_handler = logging.StreamHandler(sys.stdout)
|
||||||
|
stdout_handler.setLevel(logging.INFO)
|
||||||
|
stdout_handler.addFilter(StdoutFilter())
|
||||||
|
stderr_handler = logging.StreamHandler(sys.stderr)
|
||||||
|
stderr_handler.setLevel(logging.WARNING)
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.setLevel(logging.INFO)
|
||||||
|
root_logger.addHandler(stdout_handler)
|
||||||
|
root_logger.addHandler(stderr_handler)
|
||||||
|
|
||||||
|
api = UffdAPI(api_url, api_user, api_secret, cache_ttl)
|
||||||
|
RequestHandler = make_requesthandler(api, base_dn, bind_password, group_filter_regex)
|
||||||
|
if socket_address is not None:
|
||||||
|
host, port = parse_network_address(socket_address)
|
||||||
|
server = socketserver.ThreadingTCPServer((host, int(port)), RequestHandler)
|
||||||
|
elif socket_path is not None:
|
||||||
|
cleanup_unix_socket(socket_path)
|
||||||
|
server = socketserver.ThreadingUnixStreamServer(socket_path, RequestHandler)
|
||||||
|
else:
|
||||||
|
server = ThreadingFilenoUnixStreamServer(socket_fd, RequestHandler)
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Pylint does not seem to understand click's decorators
|
||||||
|
# pylint: disable=unexpected-keyword-arg,no-value-for-parameter
|
||||||
|
main(auto_envvar_prefix='SERVER')
|
Loading…
Add table
Add a link
Reference in a new issue