forked from Github-Mirrors/canaille
The LDAP Backend is now a class
This commit is contained in:
parent
c27f0ccdab
commit
30282e633b
7 changed files with 204 additions and 160 deletions
|
@ -174,11 +174,11 @@ def create_app(config=None, validate=True):
|
||||||
sentry_sdk = setup_sentry(app)
|
sentry_sdk = setup_sentry(app)
|
||||||
try:
|
try:
|
||||||
from .oidc.oauth import setup_oauth
|
from .oidc.oauth import setup_oauth
|
||||||
from .backends.ldap.backend import init_backend
|
from .backends.ldap.backend import LDAPBackend
|
||||||
from .app.i18n import setup_i18n
|
from .app.i18n import setup_i18n
|
||||||
|
|
||||||
setup_logging(app)
|
setup_logging(app)
|
||||||
init_backend(app)
|
LDAPBackend(app)
|
||||||
setup_oauth(app)
|
setup_oauth(app)
|
||||||
setup_blueprints(app)
|
setup_blueprints(app)
|
||||||
setup_jinja(app)
|
setup_jinja(app)
|
||||||
|
|
|
@ -9,18 +9,16 @@ from flask.cli import with_appcontext
|
||||||
def with_backendcontext(func):
|
def with_backendcontext(func):
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def _func(*args, **kwargs):
|
def _func(*args, **kwargs):
|
||||||
from canaille.backends.ldap.backend import (
|
from canaille.backends.ldap.backend import LDAPBackend
|
||||||
setup_backend,
|
|
||||||
teardown_backend,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not current_app.config["TESTING"]: # pragma: no cover
|
if not current_app.config["TESTING"]: # pragma: no cover
|
||||||
setup_backend(current_app)
|
backend = LDAPBackend(current_app)
|
||||||
|
backend.setup(current_app)
|
||||||
result = func(*args, **kwargs)
|
result = func(*args, **kwargs)
|
||||||
|
backend.teardown(current_app)
|
||||||
|
|
||||||
if not current_app.config["TESTING"]: # pragma: no cover
|
else:
|
||||||
teardown_backend(current_app)
|
result = func(*args, **kwargs)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
@ -16,9 +16,9 @@ def validate(config, validate_remote=False):
|
||||||
if not validate_remote:
|
if not validate_remote:
|
||||||
return
|
return
|
||||||
|
|
||||||
from canaille.backends.ldap.backend import validate_configuration
|
from canaille.backends.ldap.backend import LDAPBackend
|
||||||
|
|
||||||
validate_configuration(config)
|
LDAPBackend.validate(config)
|
||||||
validate_smtp_configuration(config)
|
validate_smtp_configuration(config)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
class Backend:
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
@self.app.before_request
|
||||||
|
def before_request():
|
||||||
|
if not app.config["TESTING"]:
|
||||||
|
return self.setup()
|
||||||
|
|
||||||
|
@self.app.after_request
|
||||||
|
def after_request(response):
|
||||||
|
if not app.config["TESTING"]:
|
||||||
|
self.teardown()
|
||||||
|
return response
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
"""
|
||||||
|
This method will be called before each http request,
|
||||||
|
it should open the connection to the backend.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def teardown(self):
|
||||||
|
"""
|
||||||
|
This method will be called after each http request,
|
||||||
|
it should close the connections to the backend.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, config):
|
||||||
|
"""
|
||||||
|
This method should validate the config part dedicated to the backend.
|
||||||
|
It should raise :class:`~canaille.configuration.ConfigurationError` when
|
||||||
|
errors are met.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
|
@ -3,51 +3,19 @@ import uuid
|
||||||
|
|
||||||
import ldap
|
import ldap
|
||||||
from canaille.app.configuration import ConfigurationException
|
from canaille.app.configuration import ConfigurationException
|
||||||
|
from canaille.backends import Backend
|
||||||
from flask import g
|
from flask import g
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
|
|
||||||
def setup_ldap_models(config):
|
class LDAPBackend(Backend):
|
||||||
from .ldapobject import LDAPObject
|
def __init__(self, app):
|
||||||
from canaille.core.models import Group
|
setup_ldap_models(app.config)
|
||||||
from canaille.core.models import User
|
super().__init__(app)
|
||||||
|
|
||||||
LDAPObject.root_dn = config["BACKENDS"]["LDAP"]["ROOT_DN"]
|
def setup(self):
|
||||||
|
|
||||||
user_base = config["BACKENDS"]["LDAP"]["USER_BASE"].replace(
|
|
||||||
f',{config["BACKENDS"]["LDAP"]["ROOT_DN"]}', ""
|
|
||||||
)
|
|
||||||
User.base = user_base
|
|
||||||
User.rdn_attribute = config["BACKENDS"]["LDAP"].get(
|
|
||||||
"USER_ID_ATTRIBUTE", User.DEFAULT_ID_ATTRIBUTE
|
|
||||||
)
|
|
||||||
object_class = config["BACKENDS"]["LDAP"].get(
|
|
||||||
"USER_CLASS", User.DEFAULT_OBJECT_CLASS
|
|
||||||
)
|
|
||||||
User.ldap_object_class = (
|
|
||||||
object_class if isinstance(object_class, list) else [object_class]
|
|
||||||
)
|
|
||||||
|
|
||||||
group_base = (
|
|
||||||
config["BACKENDS"]["LDAP"]
|
|
||||||
.get("GROUP_BASE", "")
|
|
||||||
.replace(f',{config["BACKENDS"]["LDAP"]["ROOT_DN"]}', "")
|
|
||||||
)
|
|
||||||
Group.base = group_base or None
|
|
||||||
Group.rdn_attribute = config["BACKENDS"]["LDAP"].get(
|
|
||||||
"GROUP_ID_ATTRIBUTE", Group.DEFAULT_ID_ATTRIBUTE
|
|
||||||
)
|
|
||||||
object_class = config["BACKENDS"]["LDAP"].get(
|
|
||||||
"GROUP_CLASS", Group.DEFAULT_OBJECT_CLASS
|
|
||||||
)
|
|
||||||
Group.ldap_object_class = (
|
|
||||||
object_class if isinstance(object_class, list) else [object_class]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_backend(app):
|
|
||||||
try: # pragma: no cover
|
try: # pragma: no cover
|
||||||
if request.endpoint == "static":
|
if request.endpoint == "static":
|
||||||
return
|
return
|
||||||
|
@ -55,18 +23,21 @@ def setup_backend(app):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
g.ldap_connection = ldap.initialize(app.config["BACKENDS"]["LDAP"]["URI"])
|
g.ldap_connection = ldap.initialize(
|
||||||
|
self.app.config["BACKENDS"]["LDAP"]["URI"]
|
||||||
|
)
|
||||||
g.ldap_connection.set_option(
|
g.ldap_connection.set_option(
|
||||||
ldap.OPT_NETWORK_TIMEOUT, app.config["BACKENDS"]["LDAP"].get("TIMEOUT")
|
ldap.OPT_NETWORK_TIMEOUT,
|
||||||
|
self.app.config["BACKENDS"]["LDAP"].get("TIMEOUT"),
|
||||||
)
|
)
|
||||||
g.ldap_connection.simple_bind_s(
|
g.ldap_connection.simple_bind_s(
|
||||||
app.config["BACKENDS"]["LDAP"]["BIND_DN"],
|
self.app.config["BACKENDS"]["LDAP"]["BIND_DN"],
|
||||||
app.config["BACKENDS"]["LDAP"]["BIND_PW"],
|
self.app.config["BACKENDS"]["LDAP"]["BIND_PW"],
|
||||||
)
|
)
|
||||||
|
|
||||||
except ldap.SERVER_DOWN:
|
except ldap.SERVER_DOWN:
|
||||||
message = _("Could not connect to the LDAP server '{uri}'").format(
|
message = _("Could not connect to the LDAP server '{uri}'").format(
|
||||||
uri=app.config["BACKENDS"]["LDAP"]["URI"]
|
uri=self.app.config["BACKENDS"]["LDAP"]["URI"]
|
||||||
)
|
)
|
||||||
logging.error(message)
|
logging.error(message)
|
||||||
return (
|
return (
|
||||||
|
@ -74,7 +45,7 @@ def setup_backend(app):
|
||||||
"error.html",
|
"error.html",
|
||||||
error=500,
|
error=500,
|
||||||
icon="database",
|
icon="database",
|
||||||
debug=app.config.get("DEBUG", False),
|
debug=self.app.config.get("DEBUG", False),
|
||||||
description=message,
|
description=message,
|
||||||
),
|
),
|
||||||
500,
|
500,
|
||||||
|
@ -82,7 +53,7 @@ def setup_backend(app):
|
||||||
|
|
||||||
except ldap.INVALID_CREDENTIALS:
|
except ldap.INVALID_CREDENTIALS:
|
||||||
message = _("LDAP authentication failed with user '{user}'").format(
|
message = _("LDAP authentication failed with user '{user}'").format(
|
||||||
user=app.config["BACKENDS"]["LDAP"]["BIND_DN"]
|
user=self.app.config["BACKENDS"]["LDAP"]["BIND_DN"]
|
||||||
)
|
)
|
||||||
logging.error(message)
|
logging.error(message)
|
||||||
return (
|
return (
|
||||||
|
@ -90,35 +61,19 @@ def setup_backend(app):
|
||||||
"error.html",
|
"error.html",
|
||||||
error=500,
|
error=500,
|
||||||
icon="key",
|
icon="key",
|
||||||
debug=app.config.get("DEBUG", False),
|
debug=self.app.config.get("DEBUG", False),
|
||||||
description=message,
|
description=message,
|
||||||
),
|
),
|
||||||
500,
|
500,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def teardown(self):
|
||||||
def teardown_backend(app):
|
|
||||||
if g.get("ldap_connection"): # pragma: no branch
|
if g.get("ldap_connection"): # pragma: no branch
|
||||||
g.ldap_connection.unbind_s()
|
g.ldap_connection.unbind_s()
|
||||||
g.ldap_connection = None
|
g.ldap_connection = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
def init_backend(app):
|
def validate(cls, config):
|
||||||
setup_ldap_models(app.config)
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def before_request():
|
|
||||||
if not app.config["TESTING"]:
|
|
||||||
return setup_backend(app)
|
|
||||||
|
|
||||||
@app.after_request
|
|
||||||
def after_request(response):
|
|
||||||
if not app.config["TESTING"]:
|
|
||||||
teardown_backend(app)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def validate_configuration(config):
|
|
||||||
from canaille.core.models import Group
|
from canaille.core.models import Group
|
||||||
from canaille.core.models import User
|
from canaille.core.models import User
|
||||||
|
|
||||||
|
@ -128,7 +83,8 @@ def validate_configuration(config):
|
||||||
ldap.OPT_NETWORK_TIMEOUT, config["BACKENDS"]["LDAP"].get("TIMEOUT")
|
ldap.OPT_NETWORK_TIMEOUT, config["BACKENDS"]["LDAP"].get("TIMEOUT")
|
||||||
)
|
)
|
||||||
conn.simple_bind_s(
|
conn.simple_bind_s(
|
||||||
config["BACKENDS"]["LDAP"]["BIND_DN"], config["BACKENDS"]["LDAP"]["BIND_PW"]
|
config["BACKENDS"]["LDAP"]["BIND_DN"],
|
||||||
|
config["BACKENDS"]["LDAP"]["BIND_PW"],
|
||||||
)
|
)
|
||||||
|
|
||||||
except ldap.SERVER_DOWN as exc:
|
except ldap.SERVER_DOWN as exc:
|
||||||
|
@ -188,3 +144,41 @@ def validate_configuration(config):
|
||||||
user.delete(conn)
|
user.delete(conn)
|
||||||
|
|
||||||
conn.unbind_s()
|
conn.unbind_s()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_ldap_models(config):
|
||||||
|
from .ldapobject import LDAPObject
|
||||||
|
from canaille.core.models import Group
|
||||||
|
from canaille.core.models import User
|
||||||
|
|
||||||
|
LDAPObject.root_dn = config["BACKENDS"]["LDAP"]["ROOT_DN"]
|
||||||
|
|
||||||
|
user_base = config["BACKENDS"]["LDAP"]["USER_BASE"].replace(
|
||||||
|
f',{config["BACKENDS"]["LDAP"]["ROOT_DN"]}', ""
|
||||||
|
)
|
||||||
|
User.base = user_base
|
||||||
|
User.rdn_attribute = config["BACKENDS"]["LDAP"].get(
|
||||||
|
"USER_ID_ATTRIBUTE", User.DEFAULT_ID_ATTRIBUTE
|
||||||
|
)
|
||||||
|
object_class = config["BACKENDS"]["LDAP"].get(
|
||||||
|
"USER_CLASS", User.DEFAULT_OBJECT_CLASS
|
||||||
|
)
|
||||||
|
User.ldap_object_class = (
|
||||||
|
object_class if isinstance(object_class, list) else [object_class]
|
||||||
|
)
|
||||||
|
|
||||||
|
group_base = (
|
||||||
|
config["BACKENDS"]["LDAP"]
|
||||||
|
.get("GROUP_BASE", "")
|
||||||
|
.replace(f',{config["BACKENDS"]["LDAP"]["ROOT_DN"]}', "")
|
||||||
|
)
|
||||||
|
Group.base = group_base or None
|
||||||
|
Group.rdn_attribute = config["BACKENDS"]["LDAP"].get(
|
||||||
|
"GROUP_ID_ATTRIBUTE", Group.DEFAULT_ID_ATTRIBUTE
|
||||||
|
)
|
||||||
|
object_class = config["BACKENDS"]["LDAP"].get(
|
||||||
|
"GROUP_CLASS", Group.DEFAULT_OBJECT_CLASS
|
||||||
|
)
|
||||||
|
Group.ldap_object_class = (
|
||||||
|
object_class if isinstance(object_class, list) else [object_class]
|
||||||
|
)
|
||||||
|
|
|
@ -14,15 +14,16 @@ def create_app():
|
||||||
|
|
||||||
@app.before_first_request
|
@app.before_first_request
|
||||||
def populate():
|
def populate():
|
||||||
from canaille.backends.ldap.backend import setup_backend
|
from canaille.backends.ldap.backend import LDAPBackend
|
||||||
from canaille.backends.ldap.backend import teardown_backend
|
|
||||||
from canaille.core.models import Group
|
from canaille.core.models import Group
|
||||||
from canaille.core.models import User
|
from canaille.core.models import User
|
||||||
from canaille.core.populate import fake_groups
|
from canaille.core.populate import fake_groups
|
||||||
from canaille.core.populate import fake_users
|
from canaille.core.populate import fake_users
|
||||||
from canaille.oidc.models import Client
|
from canaille.oidc.models import Client
|
||||||
|
|
||||||
setup_backend(app)
|
backend = LDAPBackend(app)
|
||||||
|
backend.setup()
|
||||||
|
|
||||||
jane = User(
|
jane = User(
|
||||||
formatted_name="Jane Doe",
|
formatted_name="Jane Doe",
|
||||||
given_name="Jane",
|
given_name="Jane",
|
||||||
|
@ -141,6 +142,6 @@ def create_app():
|
||||||
fake_users(50)
|
fake_users(50)
|
||||||
fake_groups(10, nb_users_max=10)
|
fake_groups(10, nb_users_max=10)
|
||||||
|
|
||||||
teardown_backend(app)
|
backend.teardown()
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
14
tests/backends/test_backends.py
Normal file
14
tests/backends/test_backends.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import pytest
|
||||||
|
from canaille.backends import Backend
|
||||||
|
|
||||||
|
|
||||||
|
def test_required_methods(testclient):
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
Backend.validate({})
|
||||||
|
|
||||||
|
backend = Backend(testclient.app)
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
backend.setup()
|
||||||
|
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
backend.teardown()
|
Loading…
Reference in a new issue