canaille-globuzma/canaille/backends/ldap/backend.py

272 lines
8.6 KiB
Python
Raw Normal View History

import logging
import os
import uuid
from contextlib import contextmanager
import ldap.modlist
import ldif
from canaille.app import models
from canaille.app.configuration import ConfigurationException
from canaille.app.i18n import gettext as _
from canaille.app.themes import render_template
2023-06-05 16:10:37 +00:00
from canaille.backends import BaseBackend
from flask import current_app
from flask import request
2023-06-22 11:09:44 +00:00
from .utils import listify
@contextmanager
def ldap_connection(config):
conn = ldap.initialize(config["BACKENDS"]["LDAP"]["URI"])
conn.set_option(ldap.OPT_NETWORK_TIMEOUT, config["BACKENDS"]["LDAP"].get("TIMEOUT"))
conn.simple_bind_s(
config["BACKENDS"]["LDAP"]["BIND_DN"], config["BACKENDS"]["LDAP"]["BIND_PW"]
)
try:
yield conn
finally:
conn.unbind_s()
def install_schema(config, schema_path):
from canaille.app.installation import InstallationException
with open(schema_path) as fd:
parser = ldif.LDIFRecordList(fd)
parser.parse()
try:
with ldap_connection(config) as conn:
for dn, entry in parser.all_records:
add_modlist = ldap.modlist.addModlist(entry)
conn.add_s(dn, add_modlist)
except ldap.INSUFFICIENT_ACCESS as exc:
raise InstallationException(
f"The user '{config['BACKENDS']['LDAP']['BIND_DN']}' has insufficient permissions to install LDAP schemas."
) from exc
2023-06-05 16:10:37 +00:00
class Backend(BaseBackend):
def __init__(self, config):
super().__init__(config)
self.connection = None
setup_ldap_models(config)
@classmethod
2023-07-01 16:46:11 +00:00
def install(cls, config, debug=False):
cls.setup_schemas(config)
with ldap_connection(config) as conn:
models.Token.install(conn)
models.AuthorizationCode.install(conn)
models.Client.install(conn)
models.Consent.install(conn)
@classmethod
def setup_schemas(cls, config):
from .ldapobject import LDAPObject
with ldap_connection(config) as conn:
if "oauthClient" not in LDAPObject.ldap_object_classes(
conn=conn, force=True
):
install_schema(
config,
os.path.dirname(__file__) + "/schemas/oauth2-openldap.ldif",
)
2023-04-08 18:42:38 +00:00
def setup(self):
2023-06-28 15:56:49 +00:00
if self.connection:
return
2023-04-08 18:42:38 +00:00
try: # pragma: no cover
if request.endpoint == "static":
return
except RuntimeError: # pragma: no cover
pass
try:
self.connection = ldap.initialize(self.config["BACKENDS"]["LDAP"]["URI"])
self.connection.set_option(
2023-04-08 18:42:38 +00:00
ldap.OPT_NETWORK_TIMEOUT,
self.config["BACKENDS"]["LDAP"].get("TIMEOUT"),
2023-04-08 18:42:38 +00:00
)
self.connection.simple_bind_s(
self.config["BACKENDS"]["LDAP"]["BIND_DN"],
self.config["BACKENDS"]["LDAP"]["BIND_PW"],
2023-04-08 18:42:38 +00:00
)
except ldap.SERVER_DOWN:
message = _("Could not connect to the LDAP server '{uri}'").format(
uri=self.config["BACKENDS"]["LDAP"]["URI"]
2023-04-08 18:42:38 +00:00
)
logging.error(message)
return (
render_template(
"error.html",
error_code=500,
2023-04-08 18:42:38 +00:00
icon="database",
description=message,
),
500,
)
except ldap.INVALID_CREDENTIALS:
message = _("LDAP authentication failed with user '{user}'").format(
user=self.config["BACKENDS"]["LDAP"]["BIND_DN"]
2023-04-08 18:42:38 +00:00
)
logging.error(message)
return (
render_template(
"error.html",
error_code=500,
2023-04-08 18:42:38 +00:00
icon="key",
description=message,
),
500,
)
def teardown(self):
2023-06-03 11:42:23 +00:00
try: # pragma: no cover
if request.endpoint == "static":
return
except RuntimeError: # pragma: no cover
pass
if self.connection: # pragma: no branch
self.connection.unbind_s()
self.connection = None
2023-04-08 18:42:38 +00:00
@classmethod
def validate(cls, config):
from canaille.app import models
2023-04-08 18:42:38 +00:00
try:
conn = ldap.initialize(config["BACKENDS"]["LDAP"]["URI"])
conn.set_option(
ldap.OPT_NETWORK_TIMEOUT, config["BACKENDS"]["LDAP"].get("TIMEOUT")
)
conn.simple_bind_s(
config["BACKENDS"]["LDAP"]["BIND_DN"],
config["BACKENDS"]["LDAP"]["BIND_PW"],
)
except ldap.SERVER_DOWN as exc:
raise ConfigurationException(
f'Could not connect to the LDAP server \'{config["BACKENDS"]["LDAP"]["URI"]}\''
) from exc
except ldap.INVALID_CREDENTIALS as exc:
raise ConfigurationException(
f'LDAP authentication failed with user \'{config["BACKENDS"]["LDAP"]["BIND_DN"]}\''
) from exc
try:
models.User.ldap_object_classes(conn)
user = models.User(
2023-04-08 18:42:38 +00:00
formatted_name=f"canaille_{uuid.uuid4()}",
family_name=f"canaille_{uuid.uuid4()}",
user_name=f"canaille_{uuid.uuid4()}",
2023-06-22 13:14:07 +00:00
emails=f"canaille_{uuid.uuid4()}@mydomain.tld",
2023-04-08 18:42:38 +00:00
password="correct horse battery staple",
)
user.save(conn)
user.delete(conn)
except ldap.INSUFFICIENT_ACCESS as exc:
raise ConfigurationException(
f'LDAP user \'{config["BACKENDS"]["LDAP"]["BIND_DN"]}\' cannot create '
f'users at \'{config["BACKENDS"]["LDAP"]["USER_BASE"]}\''
) from exc
try:
models.Group.ldap_object_classes(conn)
2023-04-08 18:42:38 +00:00
user = models.User(
2023-04-08 18:42:38 +00:00
cn=f"canaille_{uuid.uuid4()}",
family_name=f"canaille_{uuid.uuid4()}",
user_name=f"canaille_{uuid.uuid4()}",
2023-06-22 13:14:07 +00:00
emails=f"canaille_{uuid.uuid4()}@mydomain.tld",
2023-04-08 18:42:38 +00:00
password="correct horse battery staple",
)
user.save(conn)
group = models.Group(
2023-04-08 18:42:38 +00:00
display_name=f"canaille_{uuid.uuid4()}",
members=[user],
)
group.save(conn)
group.delete(conn)
except ldap.INSUFFICIENT_ACCESS as exc:
raise ConfigurationException(
f'LDAP user \'{config["BACKENDS"]["LDAP"]["BIND_DN"]}\' cannot create '
f'groups at \'{config["BACKENDS"]["LDAP"]["GROUP_BASE"]}\''
) from exc
finally:
user.delete(conn)
conn.unbind_s()
@classmethod
def login_placeholder(cls):
user_filter = current_app.config["BACKENDS"]["LDAP"].get(
"USER_FILTER", models.User.DEFAULT_FILTER
)
placeholders = []
2023-07-04 16:34:16 +00:00
if "cn={{login" in user_filter.replace(" ", ""):
placeholders.append(_("John Doe"))
2023-07-04 16:34:16 +00:00
if "uid={{login" in user_filter.replace(" ", ""):
placeholders.append(_("jdoe"))
2023-07-04 16:34:16 +00:00
if "mail={{login" in user_filter.replace(" ", "") or not placeholders:
placeholders.append(_("john@doe.com"))
return _(" or ").join(placeholders)
2022-11-01 11:25:21 +00:00
def has_account_lockability(self):
from .ldapobject import LDAPObject
try:
return "pwdEndTime" in LDAPObject.ldap_object_attributes()
2023-11-16 17:06:23 +00:00
except ldap.SERVER_DOWN: # pragma: no cover
return False
2022-11-01 11:25:21 +00:00
2023-04-08 18:42:38 +00:00
def setup_ldap_models(config):
from .ldapobject import LDAPObject
from canaille.app import models
LDAPObject.root_dn = config["BACKENDS"]["LDAP"]["ROOT_DN"]
user_base = config["BACKENDS"]["LDAP"]["USER_BASE"].replace(
f',{config["BACKENDS"]["LDAP"]["ROOT_DN"]}', ""
)
models.User.base = user_base
models.User.rdn_attribute = config["BACKENDS"]["LDAP"].get(
"USER_RDN", models.User.DEFAULT_RDN
)
object_class = config["BACKENDS"]["LDAP"].get(
"USER_CLASS", models.User.DEFAULT_OBJECT_CLASS
)
2023-06-22 11:09:44 +00:00
models.User.ldap_object_class = listify(object_class)
2022-12-13 18:04:33 +00:00
group_base = (
config["BACKENDS"]["LDAP"]
.get("GROUP_BASE", "")
.replace(f',{config["BACKENDS"]["LDAP"]["ROOT_DN"]}', "")
2022-12-13 18:04:33 +00:00
)
models.Group.base = group_base or None
models.Group.rdn_attribute = config["BACKENDS"]["LDAP"].get(
"GROUP_RDN", models.Group.DEFAULT_RDN
)
object_class = config["BACKENDS"]["LDAP"].get(
"GROUP_CLASS", models.Group.DEFAULT_OBJECT_CLASS
)
2023-06-22 11:09:44 +00:00
models.Group.ldap_object_class = listify(object_class)