diff --git a/radicale/acl/IMAP.py b/radicale/acl/IMAP.py new file mode 100644 index 0000000..e944f4d --- /dev/null +++ b/radicale/acl/IMAP.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2012 Daniel Aleksandersen +# Copyright © 2011 Corentin Le Bail +# Copyright © 2011 Guillaume Ayoub +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +""" +IMAP ACL. + +Secure authentication based on the ``imaplib`` module. + +Validating users against a modern IMAP4rev1 server that awaits STARTTLS on +port 143. Legacy SSL (often on legacy port 993) is deprecated and thus +unsupported. STARTTLS is enforced except if host is ``localhost`` as +passwords are sent in PLAIN. + +Python 3.2 or newer is required for TLS. + +""" + +import imaplib + +from radicale import acl, config, log + +IMAP_SERVER = config.get("acl", "imap_auth_host_name") +IMAP_SERVER_PORT = config.get("acl", "imap_auth_host_port") + +def has_right(owner, user, password): + """Check if ``user``/``password`` couple is valid.""" + + if not user or (owner not in acl.PRIVATE_USERS and user != owner): + # No user given, or owner is not private and is not user, forbidden + return False + + log.LOGGER.debug("[IMAP ACL] Connecting to %s:%s." % (IMAP_SERVER, IMAP_SERVER_PORT,)) + connection = imaplib.IMAP4(host=IMAP_SERVER, port=IMAP_SERVER_PORT) + + def secure_connection(): + try: + connection.starttls() + log.LOGGER.debug("[IMAP ACL] Server connection changed to TLS.") + return True + except (IMAP4.error, IAMP4.abort) as err: + import sys + PY_MIN = float(3.2) + PY_SYS = float("%s.%s" % (sys.version_info.major, sys.version_info.minor)) + if PY_SYS < PY_MIN: + log.LOGGER.error("[IMAP ACL] Python 3.2 or newer is required for TLS.") + log.LOGGER.warning("[IMAP ACL] Server at %s failed to accept TLS connection because of: %s" % (IMAP_SERVER, str(err))) + return False # server is not secure + + def server_is_local(): + if IMAP_HOST == "localhost": + log.LOGGER.warning("[IMAP ACL] Server is local. Will allow transmitting unencrypted credentials.") + return True + return False # server is not local + + """Enforcing security policy of only transmitting credentials over TLS or to local server.""" + if secure_connection() or server_is_local(): + try: + connection.login( user, password ) + connection.logout() + log.LOGGER.debug("[IMAP ACL] Authenticated user %s via %s." % (user, IMAP_SERVER)) + return True + except ( imaplib.IMAP4.error, imaplib.IMAP4.abort, Exception ) as err: + log.LOGGER.error("[IMAP ACL] Server could not authenticate user %s because of: %s" % (user, str(err))) + else: + log.LOGGER.critical("[IMAP ACL] Server did not support TLS and is not ``localhost``. Refusing to transmit passwords under these conditions. Authentication attempt aborted.") + return False # authentication failed diff --git a/radicale/config.py b/radicale/config.py index fc545c1..d667771 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -55,6 +55,8 @@ INITIAL_CONFIG = { "private_users": "private", "htpasswd_filename": "/etc/radicale/users", "htpasswd_encryption": "crypt", + "imap_auth_host_name": "localhost", + "imap_auth_host_port": "143", "ldap_url": "ldap://localhost:389/", "ldap_base": "ou=users,dc=example,dc=com", "ldap_attribute": "uid",