-
Notifications
You must be signed in to change notification settings - Fork 27
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
Initial support for Radicale 3 #13
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
# Copyright © 2011-2013 Guillaume Ayoub | ||
# Copyright © 2015 Raoul Thill | ||
# Copyright © 2017 Marco Huenseler | ||
# Copyright © 2020 Johannes Zellner | ||
# | ||
# This library is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License as published by | ||
|
@@ -25,79 +26,126 @@ | |
(https://github.com/cannatag/ldap3/). | ||
""" | ||
|
||
|
||
import ldap3 | ||
import ldap3.core.exceptions | ||
|
||
from radicale.auth import BaseAuth | ||
from radicale.log import logger | ||
|
||
import radicale_auth_ldap.ldap3imports | ||
PLUGIN_CONFIG_SCHEMA = { | ||
"auth": { | ||
"password": { | ||
"value": "", | ||
"type": str | ||
}, | ||
"ldap_url": { | ||
"value": "ldap://localhost:389", | ||
"help": "LDAP server URL, with protocol and port", | ||
"type": str | ||
}, | ||
"ldap_base": { | ||
"value": "ou=users,dc=example", | ||
"help": "LDAP base DN for users", | ||
"type": str | ||
}, | ||
"ldap_filter": { | ||
"value": "(&(objectclass=user)(username=%username))", | ||
"help": "LDAP search filter to find login user", | ||
"type": str | ||
}, | ||
"ldap_attribute": { | ||
"value": "username", | ||
"help": "LDAP attribute to uniquely identify the user", | ||
"type": str | ||
}, | ||
"ldap_binddn": { | ||
"value": "", | ||
"help": "LDAP dn used if server does not allow anonymous search", | ||
"type": str | ||
}, | ||
"ldap_password": { | ||
"value": "", | ||
"help": "LDAP password used with ldap_binddn", | ||
"type": str | ||
} | ||
} | ||
} | ||
|
||
|
||
class Auth(BaseAuth): | ||
def is_authenticated(self, user, password): | ||
"""Check if ``user``/``password`` couple is valid.""" | ||
SERVER = ldap3.Server(self.configuration.get("auth", "ldap_url")) | ||
BASE = self.configuration.get("auth", "ldap_base") | ||
ATTRIBUTE = self.configuration.get("auth", "ldap_attribute") | ||
FILTER = self.configuration.get("auth", "ldap_filter") | ||
BINDDN = self.configuration.get("auth", "ldap_binddn") | ||
PASSWORD = self.configuration.get("auth", "ldap_password") | ||
SCOPE = self.configuration.get("auth", "ldap_scope") | ||
SUPPORT_EXTENDED = self.configuration.getboolean("auth", "ldap_support_extended", fallback=True) | ||
|
||
if BINDDN and PASSWORD: | ||
conn = ldap3.Connection(SERVER, BINDDN, PASSWORD) | ||
else: | ||
conn = ldap3.Connection(SERVER) | ||
ldap_url = "" | ||
ldap_base = "" | ||
ldap_filter = "" | ||
ldap_attribute = "" | ||
ldap_binddn = "" | ||
ldap_password = "" | ||
|
||
def __init__(self, configuration): | ||
super().__init__(configuration.copy(PLUGIN_CONFIG_SCHEMA)) | ||
|
||
options = configuration.options("auth") | ||
|
||
if "ldap_url" not in options: raise RuntimeError("The ldap_url configuration for ldap auth is required.") | ||
if "ldap_base" not in options: raise RuntimeError("The ldap_base configuration for ldap auth is required.") | ||
if "ldap_filter" not in options: raise RuntimeError("The ldap_filter configuration for ldap auth is required.") | ||
if "ldap_attribute" not in options: raise RuntimeError("The ldap_attribute configuration for ldap auth is required.") | ||
if "ldap_binddn" not in options: raise RuntimeError("The ldap_binddn configuration for ldap auth is required.") | ||
if "ldap_password" not in options: raise RuntimeError("The ldap_password configuration for ldap auth is required.") | ||
|
||
# also get rid of trailing slashes which are typical for uris | ||
self.ldap_url = configuration.get("auth", "ldap_url").rstrip("/") | ||
self.ldap_base = configuration.get("auth", "ldap_base") | ||
self.ldap_filter = configuration.get("auth", "ldap_filter") | ||
self.ldap_attribute = configuration.get("auth", "ldap_attribute") | ||
self.ldap_binddn = configuration.get("auth", "ldap_binddn") | ||
self.ldap_password = configuration.get("auth", "ldap_password") | ||
|
||
logger.info("LDAP auth configuration:") | ||
logger.info(" %r is %r", "ldap_url", self.ldap_url) | ||
logger.info(" %r is %r", "ldap_base", self.ldap_base) | ||
logger.info(" %r is %r", "ldap_filter", self.ldap_filter) | ||
logger.info(" %r is %r", "ldap_attribute", self.ldap_attribute) | ||
logger.info(" %r is %r", "ldap_binddn", self.ldap_binddn) | ||
logger.info(" %r is %r", "ldap_password", self.ldap_password) | ||
|
||
def login(self, login, password): | ||
if login == "" or password == "": | ||
return "" | ||
|
||
server = ldap3.Server(self.ldap_url, get_info=ldap3.ALL) | ||
conn = ldap3.Connection(server=server, user=self.ldap_binddn, | ||
password=self.ldap_password, check_names=True, | ||
lazy=False, raise_exceptions=False) | ||
conn.open() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this actually needed? The old code didn't |
||
conn.bind() | ||
|
||
try: | ||
self.logger.debug("LDAP whoami: %s" % conn.extend.standard.who_am_i()) | ||
except Exception as err: | ||
self.logger.debug("LDAP error: %s" % err) | ||
|
||
distinguished_name = "%s=%s" % (ATTRIBUTE, ldap3imports.escape_attribute_value(user)) | ||
self.logger.debug("LDAP bind for %s in base %s" % (distinguished_name, BASE)) | ||
|
||
if FILTER: | ||
filter_string = "(&(%s)%s)" % (distinguished_name, FILTER) | ||
else: | ||
filter_string = distinguished_name | ||
self.logger.debug("LDAP filter: %s" % filter_string) | ||
|
||
conn.search(search_base=BASE, | ||
search_scope=SCOPE, | ||
search_filter=filter_string, | ||
attributes=[ATTRIBUTE]) | ||
|
||
users = conn.response | ||
|
||
if users: | ||
user_dn = users[0]['dn'] | ||
uid = users[0]['attributes'][ATTRIBUTE] | ||
self.logger.debug("LDAP user %s (%s) found" % (uid, user_dn)) | ||
try: | ||
conn = ldap3.Connection(SERVER, user_dn, password) | ||
conn.bind() | ||
self.logger.debug(conn.result) | ||
if SUPPORT_EXTENDED: | ||
whoami = conn.extend.standard.who_am_i() | ||
self.logger.debug("LDAP whoami: %s" % whoami) | ||
else: | ||
self.logger.debug("LDAP skip extended: call whoami") | ||
whoami = conn.result['result'] == 0 | ||
if whoami: | ||
self.logger.debug("LDAP bind OK") | ||
return True | ||
else: | ||
self.logger.debug("LDAP bind failed") | ||
return False | ||
except ldap3.core.exceptions.LDAPInvalidCredentialsResult: | ||
self.logger.debug("LDAP invalid credentials") | ||
except Exception as err: | ||
self.logger.debug("LDAP error %s" % err) | ||
return False | ||
else: | ||
self.logger.debug("LDAP user %s not found" % user) | ||
return False | ||
if conn.result["result"] != 0: | ||
logger.error(conn.result) | ||
return "" | ||
|
||
final_search_filter = self.ldap_filter.replace("%username", login) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I understand it, login is user-specified, right (I don't know a lot about radicale 3's plugin interface either 😆 )? In case my assumption is right, this looks very dangerous. I'm not so sure about escaping ldap-queries but I'm almost sure submitting user-controlled content directly inside the query string is a bad idea. Could you have a look at #4 ? According to ldap3's documentation it seems like |
||
conn.search(search_base=self.ldap_base, | ||
search_filter=final_search_filter, | ||
attributes=ldap3.ALL_ATTRIBUTES) | ||
|
||
if conn.result["result"] != 0: | ||
logger.error(conn.result) | ||
return "" | ||
|
||
if len(conn.response) == 0: | ||
return "" | ||
|
||
final_user_dn = conn.response[0]["dn"] | ||
conn.unbind() | ||
|
||
# new connection to check the password as we cannot rebind here | ||
conn = ldap3.Connection(server=server, user=final_user_dn, | ||
password=password, check_names=True, | ||
lazy=False, raise_exceptions=False) | ||
conn.open() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above |
||
conn.bind() | ||
|
||
if conn.result["result"] != 0: | ||
logger.error(conn.result) | ||
return "" | ||
|
||
return login |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,12 @@ | ||
#!/usr/bin/env python3 | ||
|
||
from setuptools import setup | ||
from distutils.core import setup | ||
|
||
setup( | ||
name="radicale-auth-ldap", | ||
version="0.1", | ||
description="LDAP Authentication Plugin for Radicale 2", | ||
version="0.2", | ||
description="LDAP Authentication Plugin for Radicale 3", | ||
author="Raoul Thill", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In case you wrote this all by yourself feel free to take the credits ;-) |
||
license="GNU GPL v3", | ||
install_requires=["radicale >= 2.0", "ldap3 >= 2.3"], | ||
install_requires=["radicale >= 3.0", "ldap3 >= 2.3"], | ||
packages=["radicale_auth_ldap"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should reference the
2
branch as there has been an unexpected additional contribution to the Radicale 2 plugin since the creation of the2-final
tag :) Maybe there are more to come