Skip to content
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

Another attempt at Radicale 3 support #14

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# What is this?
This is an authentication plugin for Radicale 2. It adds an LDAP authentication backend which can be used for authenticating users against an LDAP server.
This is an authentication plugin for Radicale 3. It adds an LDAP authentication backend which can be used for authenticating users against an LDAP server.

Use the `2-final` git tag for Radicale 2 support.

# How to configure
You will need to set a few options inside your radicale config file. Example:
Expand All @@ -8,7 +10,7 @@ You will need to set a few options inside your radicale config file. Example:
[auth]
type = radicale_auth_ldap

# LDAP server URL, with protocol and port
# LDAP server URL, with protocol and port (multiple servers can be separated by spaces)
ldap_url = ldap://ldap:389

# LDAP base path
Expand All @@ -33,6 +35,17 @@ ldap_password = verysecurepassword
ldap_scope = LEVEL

# LDAP extended option
# If the server is samba, ldap_support_extended is should be no
# If the server is samba, ldap_support_extended should be no
ldap_support_extended = yes
```

## SELinux considerations
If you use SELinux, you will need to add a few rules. To install `radicale-auth-ldap.te`, use these commands.

sudo checkmodule -M -m -o radicale-auth-ldap.mod radicale-auth-ldap.te && sudo semodule_package -o radicale-auth-ldap.pp -m radicale-auth-ldap.mod && sudo semodule -i radicale-auth-ldap.pp

You will need packages to run the above commands:

* checkpolicy
* policycoreutils-python (CentOS 7)
* policycoreutils (CentOS 7, AlmaLinux 8, Fedora)
19 changes: 19 additions & 0 deletions radicale-auth-ldap.te
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

module radicale-auth-ldap 1.0;

require {
type radicale_t;
type ldap_port_t;
type httpd_t;
type proc_net_t;
class capability net_admin;
class tcp_socket name_connect;
class file read;
}

#============= httpd_t ==============
allow httpd_t self:capability net_admin;

#============= radicale_t ==============
allow radicale_t ldap_port_t:tcp_socket name_connect;
allow radicale_t proc_net_t:file read;
182 changes: 144 additions & 38 deletions radicale_auth_ldap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,74 +30,180 @@
import ldap3.core.exceptions

from radicale.auth import BaseAuth
from radicale.log import logger

import radicale_auth_ldap.ldap3imports


PLUGIN_CONFIG_SCHEMA = {
"auth": {
"ldap_url": {
"value": "ldap://localhost:389",
"help": "LDAP server URL, with protocol and port (multiple servers can be separated by spaces)",
"type": str
},
"ldap_base": {
"value": "ou=users,dc=example,dc=com",
"help": "LDAP base path when searching 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
},
"ldap_scope": {
"value": "LEVEL",
"help": "scope of the search, either BASE, LEVEL or SUBTREE",
"type": str
},
"ldap_support_extended": {
"value": True,
"help": "",
"type": bool
}
}
}


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)

ldap_url = ""
ldap_base = ""
ldap_filter = ""
ldap_attribute = "user"
ldap_binddn = ""
ldap_password = ""
ldap_scope = "LEVEL"
ldap_support_extended = True

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.")

# 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")
try:
self.ldap_filter = configuration.get("auth", "ldap_filter")
except KeyError:
pass
try:
self.ldap_attribute = configuration.get("auth", "ldap_attribute")
except KeyError:
pass
try:
self.ldap_binddn = configuration.get("auth", "ldap_binddn")
except KeyError:
pass
try:
self.ldap_password = configuration.get("auth", "ldap_password")
except KeyError:
pass
try:
self.ldap_scope = configuration.get("auth", "ldap_scope")
except KeyError:
pass
try:
self.ldap_support_extended = configuration.get("auth", "ldap_support_extended")
except KeyError:
pass

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)
logger.info(" %r is %r", "ldap_scope", self.ldap_scope)
logger.info(" %r is %r", "ldap_support_extended", self.ldap_support_extended)

def login(self, login, password):
"""Check if ``login``/``password`` couple is valid."""
servers = self.configuration.get("auth", "ldap_url")
if ' ' in servers: # Handle for multiple LDAP server defined in ldap_url with space separation
servers = servers.split(' ')
logger.debug("Multiple servers: %s" % servers)
SERVER = ldap3.ServerPool(None)
for s in servers:
SERVER.add(ldap3.Server(s))
else: # only one server is defined
logger.debug("Single server: %s" % servers)
SERVER = ldap3.Server(servers)
if self.ldap_binddn and self.ldap_password:
conn = ldap3.Connection(SERVER, self.ldap_binddn, self.ldap_password)
else:
conn = ldap3.Connection(SERVER)
conn.bind()

try:
self.logger.debug("LDAP whoami: %s" % conn.extend.standard.who_am_i())
logger.debug("LDAP whoami: %s" % conn.extend.standard.who_am_i())
except Exception as err:
self.logger.debug("LDAP error: %s" % err)
logger.error("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))
distinguished_name = "%s=%s" % (self.ldap_attribute, ldap3imports.escape_attribute_value(login))
logger.debug("LDAP bind for %s in base %s" % (distinguished_name, self.ldap_base))

if FILTER:
filter_string = "(&(%s)%s)" % (distinguished_name, FILTER)
if self.ldap_filter:
filter_string = "(&(%s)%s)" % (distinguished_name, self.ldap_filter)
else:
filter_string = distinguished_name
self.logger.debug("LDAP filter: %s" % filter_string)
logger.debug("LDAP filter: %s" % filter_string)

conn.search(search_base=BASE,
search_scope=SCOPE,
conn.search(search_base=self.ldap_base,
search_scope=self.ldap_scope,
search_filter=filter_string,
attributes=[ATTRIBUTE])
attributes=[self.ldap_attribute])

users = conn.response
conn.unbind()

if users:
user_dn = users[0]['dn']
uid = users[0]['attributes'][ATTRIBUTE]
self.logger.debug("LDAP user %s (%s) found" % (uid, user_dn))
uid = users[0]['attributes'][self.ldap_attribute]
logger.info("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:
logger.debug(conn.result)
if self.ldap_support_extended:
whoami = conn.extend.standard.who_am_i()
self.logger.debug("LDAP whoami: %s" % whoami)
logger.debug("LDAP whoami: %s" % whoami)
else:
self.logger.debug("LDAP skip extended: call whoami")
logger.debug("LDAP skip extended: call whoami")
whoami = conn.result['result'] == 0
conn.unbind()
if whoami:
self.logger.debug("LDAP bind OK")
return True
logger.info("LDAP bind OK")
return login
else:
self.logger.debug("LDAP bind failed")
return False
logger.error("LDAP bind failed")
return ""
except ldap3.core.exceptions.LDAPInvalidCredentialsResult:
self.logger.debug("LDAP invalid credentials")
logger.error("LDAP invalid credentials")
except Exception as err:
self.logger.debug("LDAP error %s" % err)
return False
logger.error("LDAP error %s" % err)
return ""
else:
self.logger.debug("LDAP user %s not found" % user)
return False
logger.error("LDAP user %s not found" % user)
return ""
8 changes: 4 additions & 4 deletions setup.py
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.3",
description="LDAP Authentication Plugin for Radicale 3",
author="Raoul Thill",
license="GNU GPL v3",
install_requires=["radicale >= 2.0", "ldap3 >= 2.3"],
install_requires=["radicale >= 3.0", "ldap3 >= 2.3"],
packages=["radicale_auth_ldap"])