Moved LDAP configuration entry to BACKENDS.LDAP

This commit is contained in:
Éloi Rivard 2023-04-10 20:31:54 +02:00
parent cc45ed4be9
commit e2b96af1ee
17 changed files with 97 additions and 71 deletions

View file

@ -11,11 +11,12 @@ Changed
- Renamed user model attributes to match SCIM naming convention. :pr:`123`
- Moved OIDC related configuration entries in ``OIDC``
- Moved ``LDAP`` configuration entry to ``BACKENDS.LDAP``
Fixed
*****
- ``OIDC.JWT.MAPPING`` is really optional now.
- ``OIDC.JWT.MAPPING`` configuration entry is really optional now.
[0.0.24] - 2023-04-07
=====================

View file

@ -25,7 +25,9 @@ def profile_hash(*args):
def login_placeholder():
user_filter = current_app.config["LDAP"].get("USER_FILTER", User.DEFAULT_FILTER)
user_filter = current_app.config["BACKENDS"]["LDAP"].get(
"USER_FILTER", User.DEFAULT_FILTER
)
placeholders = []
if "cn={login}" in user_filter:

View file

@ -55,7 +55,7 @@ SECRET_KEY = "change me before you go in production"
# written in the standard error output.
# PATH = ""
[LDAP]
[BACKENDS.LDAP]
URI = "ldap://ldap"
ROOT_DN = "dc=mydomain,dc=tld"
BIND_DN = "cn=admin,dc=mydomain,dc=tld"

View file

@ -45,7 +45,7 @@ class User(LDAPObject):
def get_from_login(cls, login=None, **kwargs):
filter = (
(
current_app.config["LDAP"]
current_app.config["BACKENDS"]["LDAP"]
.get("USER_FILTER", User.DEFAULT_FILTER)
.format(login=ldap.filter.escape_filter_chars(login))
)
@ -65,7 +65,7 @@ class User(LDAPObject):
def load_groups(self):
group_filter = (
current_app.config["LDAP"]
current_app.config["BACKENDS"]["LDAP"]
.get("GROUP_USER_FILTER", Group.DEFAULT_USER_FILTER)
.format(user=self)
)
@ -107,10 +107,11 @@ class User(LDAPObject):
return bool(self.password)
def check_password(self, password):
conn = ldap.initialize(current_app.config["LDAP"]["URI"])
conn = ldap.initialize(current_app.config["BACKENDS"]["LDAP"]["URI"])
conn.set_option(
ldap.OPT_NETWORK_TIMEOUT, current_app.config["LDAP"].get("TIMEOUT")
ldap.OPT_NETWORK_TIMEOUT,
current_app.config["BACKENDS"]["LDAP"].get("TIMEOUT"),
)
try:
@ -213,7 +214,7 @@ class Group(LDAPObject):
@property
def display_name(self):
attribute = current_app.config["LDAP"].get(
attribute = current_app.config["BACKENDS"]["LDAP"].get(
"GROUP_NAME_ATTRIBUTE", Group.DEFAULT_NAME_ATTRIBUTE
)
return self[attribute][0]

View file

@ -14,28 +14,34 @@ def setup_ldap_models(config):
from canaille.core.models import Group
from canaille.core.models import User
LDAPObject.root_dn = config["LDAP"]["ROOT_DN"]
LDAPObject.root_dn = config["BACKENDS"]["LDAP"]["ROOT_DN"]
user_base = config["LDAP"]["USER_BASE"].replace(f',{config["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["LDAP"].get(
User.rdn_attribute = config["BACKENDS"]["LDAP"].get(
"USER_ID_ATTRIBUTE", User.DEFAULT_ID_ATTRIBUTE
)
object_class = config["LDAP"].get("USER_CLASS", User.DEFAULT_OBJECT_CLASS)
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["LDAP"]
config["BACKENDS"]["LDAP"]
.get("GROUP_BASE", "")
.replace(f',{config["LDAP"]["ROOT_DN"]}', "")
.replace(f',{config["BACKENDS"]["LDAP"]["ROOT_DN"]}', "")
)
Group.base = group_base or None
Group.rdn_attribute = config["LDAP"].get(
Group.rdn_attribute = config["BACKENDS"]["LDAP"].get(
"GROUP_ID_ATTRIBUTE", Group.DEFAULT_ID_ATTRIBUTE
)
object_class = config["LDAP"].get("GROUP_CLASS", Group.DEFAULT_OBJECT_CLASS)
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]
)
@ -49,17 +55,18 @@ def setup_backend(app):
pass
try:
g.ldap_connection = ldap.initialize(app.config["LDAP"]["URI"])
g.ldap_connection = ldap.initialize(app.config["BACKENDS"]["LDAP"]["URI"])
g.ldap_connection.set_option(
ldap.OPT_NETWORK_TIMEOUT, app.config["LDAP"].get("TIMEOUT")
ldap.OPT_NETWORK_TIMEOUT, app.config["BACKENDS"]["LDAP"].get("TIMEOUT")
)
g.ldap_connection.simple_bind_s(
app.config["LDAP"]["BIND_DN"], app.config["LDAP"]["BIND_PW"]
app.config["BACKENDS"]["LDAP"]["BIND_DN"],
app.config["BACKENDS"]["LDAP"]["BIND_PW"],
)
except ldap.SERVER_DOWN:
message = _("Could not connect to the LDAP server '{uri}'").format(
uri=app.config["LDAP"]["URI"]
uri=app.config["BACKENDS"]["LDAP"]["URI"]
)
logging.error(message)
return (
@ -75,7 +82,7 @@ def setup_backend(app):
except ldap.INVALID_CREDENTIALS:
message = _("LDAP authentication failed with user '{user}'").format(
user=app.config["LDAP"]["BIND_DN"]
user=app.config["BACKENDS"]["LDAP"]["BIND_DN"]
)
logging.error(message)
return (
@ -116,18 +123,22 @@ def validate_configuration(config):
from canaille.core.models import User
try:
conn = ldap.initialize(config["LDAP"]["URI"])
conn.set_option(ldap.OPT_NETWORK_TIMEOUT, config["LDAP"].get("TIMEOUT"))
conn.simple_bind_s(config["LDAP"]["BIND_DN"], config["LDAP"]["BIND_PW"])
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["LDAP"]["URI"]}\''
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["LDAP"]["BIND_DN"]}\''
f'LDAP authentication failed with user \'{config["BACKENDS"]["LDAP"]["BIND_DN"]}\''
) from exc
try:
@ -144,8 +155,8 @@ def validate_configuration(config):
except ldap.INSUFFICIENT_ACCESS as exc:
raise ConfigurationException(
f'LDAP user \'{config["LDAP"]["BIND_DN"]}\' cannot create '
f'users at \'{config["LDAP"]["USER_BASE"]}\''
f'LDAP user \'{config["BACKENDS"]["LDAP"]["BIND_DN"]}\' cannot create '
f'users at \'{config["BACKENDS"]["LDAP"]["USER_BASE"]}\''
) from exc
try:
@ -169,8 +180,8 @@ def validate_configuration(config):
except ldap.INSUFFICIENT_ACCESS as exc:
raise ConfigurationException(
f'LDAP user \'{config["LDAP"]["BIND_DN"]}\' cannot create '
f'groups at \'{config["LDAP"]["GROUP_BASE"]}\''
f'LDAP user \'{config["BACKENDS"]["LDAP"]["BIND_DN"]}\' cannot create '
f'groups at \'{config["BACKENDS"]["LDAP"]["GROUP_BASE"]}\''
) from exc
finally:

View file

@ -6,9 +6,11 @@ import ldif
@contextmanager
def ldap_connection(config):
conn = ldap.initialize(config["LDAP"]["URI"])
conn.set_option(ldap.OPT_NETWORK_TIMEOUT, config["LDAP"].get("TIMEOUT"))
conn.simple_bind_s(config["LDAP"]["BIND_DN"], config["LDAP"]["BIND_PW"])
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
@ -31,5 +33,5 @@ def install_schema(config, schema_path):
except ldap.INSUFFICIENT_ACCESS as exc:
raise InstallationException(
f"The user '{config['LDAP']['BIND_DN']}' has insufficient permissions to install LDAP schemas."
f"The user '{config['BACKENDS']['LDAP']['BIND_DN']}' has insufficient permissions to install LDAP schemas."
) from exc

View file

@ -56,7 +56,7 @@ LEVEL = "DEBUG"
# written in the standard error output.
# PATH = ""
[LDAP]
[BACKENDS.LDAP]
URI = "ldap://ldap:389"
ROOT_DN = "dc=mydomain,dc=tld"
BIND_DN = "cn=admin,dc=mydomain,dc=tld"

View file

@ -56,7 +56,7 @@ LEVEL = "DEBUG"
# written in the standard error output.
# PATH = ""
[LDAP]
[BACKENDS.LDAP]
URI = "ldap://127.0.0.1:5389"
ROOT_DN = "dc=mydomain,dc=tld"
BIND_DN = "cn=admin,dc=mydomain,dc=tld"

View file

@ -60,8 +60,8 @@ LOGGING
:PATH:
*Optional.* The log file path. If not set, logs are written in the standard error output.
LDAP
----
BACKENDS.LDAP
-------------
:URI:
**Required.** The URI to the LDAP server.

View file

@ -8,7 +8,7 @@ def test_check_command(testclient):
def test_check_command_fail(testclient):
testclient.app.config["LDAP"]["URI"] = "ldap://invalid-ldap.com"
testclient.app.config["BACKENDS"]["LDAP"]["URI"] = "ldap://invalid-ldap.com"
runner = testclient.app.test_cli_runner()
res = runner.invoke(cli, ["check"])
assert res.exit_code == 1, res.stdout

View file

@ -18,7 +18,7 @@ def test_ldap_connection_remote(testclient, configuration, slapd_connection):
def test_ldap_connection_remote_ldap_unreachable(testclient, configuration):
configuration["LDAP"]["URI"] = "ldap://invalid-ldap.com"
configuration["BACKENDS"]["LDAP"]["URI"] = "ldap://invalid-ldap.com"
with pytest.raises(
ConfigurationException,
match=r"Could not connect to the LDAP server",
@ -27,7 +27,7 @@ def test_ldap_connection_remote_ldap_unreachable(testclient, configuration):
def test_ldap_connection_remote_ldap_wrong_credentials(testclient, configuration):
configuration["LDAP"]["BIND_PW"] = "invalid-password"
configuration["BACKENDS"]["LDAP"]["BIND_PW"] = "invalid-password"
with pytest.raises(
ConfigurationException,
match=r"LDAP authentication failed with user",

View file

@ -1,7 +1,7 @@
def test_ldap_connection_remote_ldap_unreachable(testclient):
testclient.app.config["TESTING"] = False
testclient.app.config["LDAP"]["URI"] = "ldap://invalid-ldap.com"
testclient.app.config["BACKENDS"]["LDAP"]["URI"] = "ldap://invalid-ldap.com"
testclient.app.config["DEBUG"] = True
res = testclient.get("/", status=500, expect_errors=True)
@ -15,7 +15,7 @@ def test_ldap_connection_remote_ldap_unreachable(testclient):
def test_ldap_connection_remote_ldap_wrong_credentials(testclient):
testclient.app.config["TESTING"] = False
testclient.app.config["LDAP"]["BIND_PW"] = "invalid-password"
testclient.app.config["BACKENDS"]["LDAP"]["BIND_PW"] = "invalid-password"
testclient.app.config["DEBUG"] = True
res = testclient.get("/", status=500, expect_errors=True)

View file

@ -33,7 +33,7 @@ def test_environment_configuration(slapd_server, configuration, tmp_path):
os.environ["CONFIG"] = config_path
app = create_app()
assert app.config["LDAP"]["ROOT_DN"] == slapd_server.suffix
assert app.config["BACKENDS"]["LDAP"]["ROOT_DN"] == slapd_server.suffix
del os.environ["CONFIG"]
os.remove(config_path)

View file

@ -78,15 +78,17 @@ def configuration(slapd_server, smtpd):
conf = {
"SECRET_KEY": gen_salt(24),
"LOGO": "/static/img/canaille-head.png",
"LDAP": {
"ROOT_DN": slapd_server.suffix,
"URI": slapd_server.ldap_uri,
"BIND_DN": slapd_server.root_dn,
"BIND_PW": slapd_server.root_pw,
"USER_BASE": "ou=users",
"USER_FILTER": "(|(uid={login})(cn={login}))",
"GROUP_BASE": "ou=groups",
"TIMEOUT": 0.1,
"BACKENDS": {
"LDAP": {
"ROOT_DN": slapd_server.suffix,
"URI": slapd_server.ldap_uri,
"BIND_DN": slapd_server.root_dn,
"BIND_PW": slapd_server.root_pw,
"USER_BASE": "ou=users",
"USER_FILTER": "(|(uid={login})(cn={login}))",
"GROUP_BASE": "ou=groups",
"TIMEOUT": 0.1,
},
},
"ACL": {
"DEFAULT": {

View file

@ -334,18 +334,20 @@ def test_user_self_deletion(testclient, slapd_connection):
def test_login_placeholder(testclient):
testclient.app.config["LDAP"]["USER_FILTER"] = "(uid={login})"
testclient.app.config["BACKENDS"]["LDAP"]["USER_FILTER"] = "(uid={login})"
placeholder = testclient.get("/login").form["login"].attrs["placeholder"]
assert placeholder == "jdoe"
testclient.app.config["LDAP"]["USER_FILTER"] = "(cn={login})"
testclient.app.config["BACKENDS"]["LDAP"]["USER_FILTER"] = "(cn={login})"
placeholder = testclient.get("/login").form["login"].attrs["placeholder"]
assert placeholder == "John Doe"
testclient.app.config["LDAP"]["USER_FILTER"] = "(mail={login})"
testclient.app.config["BACKENDS"]["LDAP"]["USER_FILTER"] = "(mail={login})"
placeholder = testclient.get("/login").form["login"].attrs["placeholder"]
assert placeholder == "john@doe.com"
testclient.app.config["LDAP"]["USER_FILTER"] = "(|(uid={login})(mail={login}))"
testclient.app.config["BACKENDS"]["LDAP"][
"USER_FILTER"
] = "(|(uid={login})(mail={login}))"
placeholder = testclient.get("/login").form["login"].attrs["placeholder"]
assert placeholder == "jdoe or john@doe.com"

View file

@ -194,7 +194,7 @@ def test_guess_object_from_dn(slapd_connection, testclient, foo_group):
def test_object_class_update(slapd_connection, testclient):
testclient.app.config["LDAP"]["USER_CLASS"] = ["inetOrgPerson"]
testclient.app.config["BACKENDS"]["LDAP"]["USER_CLASS"] = ["inetOrgPerson"]
setup_ldap_models(testclient.app.config)
user1 = User(cn="foo1", sn="bar1")
@ -203,7 +203,10 @@ def test_object_class_update(slapd_connection, testclient):
assert user1.objectClass == ["inetOrgPerson"]
assert User.get(id=user1.id).objectClass == ["inetOrgPerson"]
testclient.app.config["LDAP"]["USER_CLASS"] = ["inetOrgPerson", "extensibleObject"]
testclient.app.config["BACKENDS"]["LDAP"]["USER_CLASS"] = [
"inetOrgPerson",
"extensibleObject",
]
setup_ldap_models(testclient.app.config)
user2 = User(cn="foo2", sn="bar2")

View file

@ -61,10 +61,10 @@ def test_install_keypair(configuration, tmpdir):
def test_install_schemas(configuration, slapd_server):
configuration["LDAP"]["ROOT_DN"] = slapd_server.suffix
configuration["LDAP"]["URI"] = slapd_server.ldap_uri
configuration["LDAP"]["BIND_DN"] = slapd_server.root_dn
configuration["LDAP"]["BIND_PW"] = slapd_server.root_pw
configuration["BACKENDS"]["LDAP"]["ROOT_DN"] = slapd_server.suffix
configuration["BACKENDS"]["LDAP"]["URI"] = slapd_server.ldap_uri
configuration["BACKENDS"]["LDAP"]["BIND_DN"] = slapd_server.root_dn
configuration["BACKENDS"]["LDAP"]["BIND_PW"] = slapd_server.root_pw
conn = ldap.ldapobject.SimpleLDAPObject(slapd_server.ldap_uri)
conn.protocol_version = 3
@ -81,10 +81,12 @@ def test_install_schemas(configuration, slapd_server):
def test_install_no_permissions_to_install_schemas(configuration, slapd_server):
configuration["LDAP"]["ROOT_DN"] = slapd_server.suffix
configuration["LDAP"]["URI"] = slapd_server.ldap_uri
configuration["LDAP"]["BIND_DN"] = "uid=admin,ou=users,dc=mydomain,dc=tld"
configuration["LDAP"]["BIND_PW"] = "admin"
configuration["BACKENDS"]["LDAP"]["ROOT_DN"] = slapd_server.suffix
configuration["BACKENDS"]["LDAP"]["URI"] = slapd_server.ldap_uri
configuration["BACKENDS"]["LDAP"][
"BIND_DN"
] = "uid=admin,ou=users,dc=mydomain,dc=tld"
configuration["BACKENDS"]["LDAP"]["BIND_PW"] = "admin"
conn = ldap.ldapobject.SimpleLDAPObject(slapd_server.ldap_uri)
conn.protocol_version = 3
@ -102,10 +104,10 @@ def test_install_no_permissions_to_install_schemas(configuration, slapd_server):
def test_install_schemas_command(configuration, slapd_server):
configuration["LDAP"]["ROOT_DN"] = slapd_server.suffix
configuration["LDAP"]["URI"] = slapd_server.ldap_uri
configuration["LDAP"]["BIND_DN"] = slapd_server.root_dn
configuration["LDAP"]["BIND_PW"] = slapd_server.root_pw
configuration["BACKENDS"]["LDAP"]["ROOT_DN"] = slapd_server.suffix
configuration["BACKENDS"]["LDAP"]["URI"] = slapd_server.ldap_uri
configuration["BACKENDS"]["LDAP"]["BIND_DN"] = slapd_server.root_dn
configuration["BACKENDS"]["LDAP"]["BIND_PW"] = slapd_server.root_pw
conn = ldap.ldapobject.SimpleLDAPObject(slapd_server.ldap_uri)
conn.protocol_version = 3