Sfoglia il codice sorgente

Remove extra auth, rights and storage modules

Guillaume Ayoub 10 anni fa
parent
commit
1001bcb676

+ 3 - 71
config

@@ -73,12 +73,9 @@
 [auth]
 
 # Authentication method
-# Value: None | htpasswd | IMAP | LDAP | PAM | courier | http | remote_user | custom
+# Value: None | htpasswd
 #type = None
 
-# Custom authentication handler
-#custom_handler =
-
 # Htpasswd filename
 #htpasswd_filename = /etc/radicale/users
 
@@ -86,65 +83,13 @@
 # Value: plain | sha1 | ssha | crypt | bcrypt | md5
 #htpasswd_encryption = crypt
 
-# LDAP server URL, with protocol and port
-#ldap_url = ldap://localhost:389/
-
-# LDAP base path
-#ldap_base = ou=users,dc=example,dc=com
-
-# LDAP login attribute
-#ldap_attribute = uid
-
-# LDAP filter string
-# placed as X in a query of the form (&(...)X)
-# example: (objectCategory=Person)(objectClass=User)(memberOf=cn=calenderusers,ou=users,dc=example,dc=org)
-# leave empty if no additional filter is needed
-#ldap_filter =
-
-# LDAP dn for initial login, used if LDAP server does not allow anonymous searches
-# Leave empty if searches are anonymous
-#ldap_binddn =
-
-# LDAP password for initial login, used with ldap_binddn
-#ldap_password =
-
-# LDAP scope of the search
-#ldap_scope = OneLevel
-
-# IMAP Configuration
-#imap_hostname = localhost
-#imap_port = 143
-#imap_ssl = False
-
-# PAM group user should be member of
-#pam_group_membership =
-
-# Path to the Courier Authdaemon socket
-#courier_socket =
-
-# HTTP authentication request URL endpoint
-#http_url =
-# POST parameter to use for username
-#http_user_parameter =
-# POST parameter to use for password
-#http_password_parameter =
-
-
-[git]
-
-# Git default options
-#committer = Radicale <radicale@example.com>
-
 
 [rights]
 
 # Rights backend
-# Value: None | authenticated | owner_only | owner_write | from_file | custom
+# Value: None | authenticated | owner_only | owner_write | from_file
 #type = None
 
-# Custom rights handler
-#custom_handler =
-
 # File for rights management from_file
 #file = ~/.config/radicale/rights
 
@@ -152,25 +97,12 @@
 [storage]
 
 # Storage backend
-# -------
-# WARNING: ONLY "filesystem" IS DOCUMENTED AND TESTED,
-#          OTHER BACKENDS ARE NOT READY FOR PRODUCTION.
-# -------
-# Value: filesystem | multifilesystem | database | custom
+# Value: multifilesystem
 #type = filesystem
 
-# Custom storage handler
-#custom_handler =
-
 # Folder for storing local collections, created if not present
 #filesystem_folder = ~/.config/radicale/collections
 
-# Database URL for SQLAlchemy
-# dialect+driver://user:password@host/dbname[?key=value..]
-# For example: sqlite:///var/db/radicale.db, postgresql://user:password@localhost/radicale
-# See http://docs.sqlalchemy.org/en/rel_0_8/core/engines.html#sqlalchemy.create_engine
-#database_url =
-
 
 [logging]
 

+ 19 - 4
radicale/auth/htpasswd.py → radicale/auth.py

@@ -17,7 +17,9 @@
 # along with Radicale.  If not, see <http://www.gnu.org/licenses/>.
 
 """
-Implement htpasswd authentication.
+Authentication management.
+
+Default is htpasswd authentication.
 
 Apache's htpasswd command (httpd.apache.org/docs/programs/htpasswd.html) manages
 a file for storing user credentials. It can encrypt passwords using different
@@ -50,13 +52,26 @@ following significantly more secure schemes are parsable by Radicale:
 
 """
 
-
 import base64
 import hashlib
 import os
+import sys
 
+from . import config, log
 
-from .. import config
+
+def _load():
+    """Load the authentication manager chosen in configuration."""
+    auth_type = config.get("auth", "type")
+    log.LOGGER.debug("Authentication type is %s" % auth_type)
+    if auth_type == "None":
+        sys.modules[__name__].is_authenticated = lambda user, password: True
+    elif auth_type == "htpasswd":
+        pass  # is_authenticated is already defined
+    else:
+        __import__(auth_type)
+        sys.modules[__name__].is_authenticated = (
+            sys.modules[auth_type].is_authenticated)
 
 
 FILENAME = os.path.expanduser(config.get("auth", "htpasswd_filename"))
@@ -144,7 +159,7 @@ elif ENCRYPTION == "crypt":
 if ENCRYPTION not in _verifuncs:
     raise RuntimeError(("The htpasswd encryption method '%s' is not "
         "supported." % ENCRYPTION))
- 
+
 
 def is_authenticated(user, password):
     """Validate credentials.

+ 0 - 97
radicale/auth/IMAP.py

@@ -1,97 +0,0 @@
-# This file is part of Radicale Server - Calendar Server
-# Copyright © 2012 Daniel Aleksandersen
-# Copyright © 2013 Nikita Koshikov
-# Copyright © 2013-2016 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 <http://www.gnu.org/licenses/>.
-
-"""
-IMAP authentication.
-
-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.
-
-"""
-
-import imaplib
-
-from .. import config, log
-
-IMAP_SERVER = config.get("auth", "imap_hostname")
-IMAP_SERVER_PORT = config.getint("auth", "imap_port")
-IMAP_USE_SSL = config.getboolean("auth", "imap_ssl")
-
-IMAP_WARNED_UNENCRYPTED = False
-
-def is_authenticated(user, password):
-    """Check if ``user``/``password`` couple is valid."""
-    global IMAP_WARNED_UNENCRYPTED
-
-    if not user or not password:
-        return False
-
-    log.LOGGER.debug(
-        "Connecting to IMAP server %s:%s." % (IMAP_SERVER, IMAP_SERVER_PORT,))
-
-    connection_is_secure = False
-    if IMAP_USE_SSL:
-        connection = imaplib.IMAP4_SSL(host=IMAP_SERVER, port=IMAP_SERVER_PORT)
-        connection_is_secure = True
-    else:
-        connection = imaplib.IMAP4(host=IMAP_SERVER, port=IMAP_SERVER_PORT)
-
-    server_is_local = (IMAP_SERVER == "localhost")
-
-    if not connection_is_secure:
-        try:
-            connection.starttls()
-            log.LOGGER.debug("IMAP server connection changed to TLS.")
-            connection_is_secure = True
-        except AttributeError:
-            if not server_is_local:
-                log.LOGGER.error(
-                    "Python 3.2 or newer is required for IMAP + TLS.")
-        except (imaplib.IMAP4.error, imaplib.IMAP4.abort) as exception:
-            log.LOGGER.warning(
-                "IMAP server at %s failed to accept TLS connection "
-                "because of: %s" % (IMAP_SERVER, exception))
-
-    if server_is_local and not connection_is_secure and not IMAP_WARNED_UNENCRYPTED:
-        IMAP_WARNED_UNENCRYPTED = True
-        log.LOGGER.warning(
-            "IMAP server is local. "
-            "Will allow transmitting unencrypted credentials.")
-
-    if connection_is_secure or server_is_local:
-        try:
-            connection.login(user, password)
-            connection.logout()
-            log.LOGGER.debug(
-                "Authenticated IMAP user %s "
-                "via %s." % (user, IMAP_SERVER))
-            return True
-        except (imaplib.IMAP4.error, imaplib.IMAP4.abort) as exception:
-            log.LOGGER.error(
-                "IMAP server could not authenticate user %s "
-                "because of: %s" % (user, exception))
-    else:
-        log.LOGGER.critical(
-            "IMAP server did not support TLS and is not ``localhost``. "
-            "Refusing to transmit passwords under these conditions. "
-            "Authentication attempt aborted.")
-    return False  # authentication failed

+ 0 - 78
radicale/auth/LDAP.py

@@ -1,78 +0,0 @@
-# This file is part of Radicale Server - Calendar Server
-# Copyright © 2011 Corentin Le Bail
-# Copyright © 2011-2016 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 <http://www.gnu.org/licenses/>.
-
-"""
-LDAP authentication.
-
-Authentication based on the ``python-ldap`` module
-(http://www.python-ldap.org/).
-
-"""
-
-import ldap
-
-from .. import config, log
-
-
-BASE = config.get("auth", "ldap_base")
-ATTRIBUTE = config.get("auth", "ldap_attribute")
-FILTER = config.get("auth", "ldap_filter")
-CONNEXION = ldap.initialize(config.get("auth", "ldap_url"))
-BINDDN = config.get("auth", "ldap_binddn")
-PASSWORD = config.get("auth", "ldap_password")
-SCOPE = getattr(ldap, "SCOPE_%s" % config.get("auth", "ldap_scope").upper())
-
-
-def is_authenticated(user, password):
-    """Check if ``user``/``password`` couple is valid."""
-    global CONNEXION
-
-    try:
-        CONNEXION.whoami_s()
-    except:
-        log.LOGGER.debug("Reconnecting the LDAP server")
-        CONNEXION = ldap.initialize(config.get("auth", "ldap_url"))
-
-    if BINDDN and PASSWORD:
-        log.LOGGER.debug("Initial LDAP bind as %s" % BINDDN)
-        CONNEXION.simple_bind_s(BINDDN, PASSWORD)
-
-    distinguished_name = "%s=%s" % (ATTRIBUTE, ldap.dn.escape_dn_chars(user))
-    log.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
-    log.LOGGER.debug("Used LDAP filter: %s" % filter_string)
-
-    users = CONNEXION.search_s(BASE, SCOPE, filter_string)
-    if users:
-        log.LOGGER.debug("User %s found" % user)
-        try:
-            CONNEXION.simple_bind_s(users[0][0], password or "")
-        except ldap.LDAPError:
-            log.LOGGER.debug("Invalid credentials")
-        else:
-            log.LOGGER.debug("LDAP bind OK")
-            return True
-    else:
-        log.LOGGER.debug("User %s not found" % user)
-
-    log.LOGGER.debug("LDAP bind failed")
-    return False

+ 0 - 93
radicale/auth/PAM.py

@@ -1,93 +0,0 @@
-# This file is part of Radicale Server - Calendar Server
-# Copyright © 2011 Henry-Nicolas Tourneur
-# Copyright © 2016 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 <http://www.gnu.org/licenses/>.
-
-"""
-PAM authentication.
-
-Authentication based on the ``pam-python`` module.
-
-"""
-
-import grp
-import pwd
-
-import pam
-
-from .. import config, log
-
-
-GROUP_MEMBERSHIP = config.get("auth", "pam_group_membership")
-
-
-# Compatibility for old versions of python-pam.
-if hasattr(pam, "pam"):
-    def pam_authenticate(*args, **kwargs):
-        return pam.pam().authenticate(*args, **kwargs)
-else:
-    def pam_authenticate(*args, **kwargs):
-        return pam.authenticate(*args, **kwargs)
-
-
-def is_authenticated(user, password):
-    """Check if ``user``/``password`` couple is valid."""
-    if user is None or password is None:
-        return False
-
-    # Check whether the user exists in the PAM system
-    try:
-        pwd.getpwnam(user).pw_uid
-    except KeyError:
-        log.LOGGER.debug("User %s not found" % user)
-        return False
-    else:
-        log.LOGGER.debug("User %s found" % user)
-
-    # Check whether the group exists
-    try:
-        # Obtain supplementary groups
-        members = grp.getgrnam(GROUP_MEMBERSHIP).gr_mem
-    except KeyError:
-        log.LOGGER.debug(
-            "The PAM membership required group (%s) doesn't exist" %
-            GROUP_MEMBERSHIP)
-        return False
-
-    # Check whether the user exists
-    try:
-        # Get user primary group
-        primary_group = grp.getgrgid(pwd.getpwnam(user).pw_gid).gr_name
-    except KeyError:
-        log.LOGGER.debug("The PAM user (%s) doesn't exist" % user)
-        return False
-
-    # Check whether the user belongs to the required group
-    # (primary or supplementary)
-    if primary_group == GROUP_MEMBERSHIP or user in members:
-        log.LOGGER.debug(
-            "The PAM user belongs to the required group (%s)" %
-            GROUP_MEMBERSHIP)
-        # Check the password
-        if pam_authenticate(user, password, service='radicale'):
-            return True
-        else:
-            log.LOGGER.debug("Wrong PAM password")
-    else:
-        log.LOGGER.debug(
-            "The PAM user doesn't belong to the required group (%s)" %
-            GROUP_MEMBERSHIP)
-
-    return False

+ 0 - 54
radicale/auth/__init__.py

@@ -1,54 +0,0 @@
-# This file is part of Radicale Server - Calendar Server
-# Copyright © 2008 Nicolas Kandel
-# Copyright © 2008 Pascal Halter
-# Copyright © 2008-2016 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 <http://www.gnu.org/licenses/>.
-
-"""
-Authentication management.
-
-"""
-
-import sys
-
-from .. import config, log
-
-
-def load():
-    """Load list of available authentication managers."""
-    auth_type = config.get("auth", "type")
-    log.LOGGER.debug("Authentication type is %s" % auth_type)
-    if auth_type == "None":
-        return None
-    elif auth_type == 'custom':
-        auth_module = config.get("auth", "custom_handler")
-        __import__(auth_module)
-        module = sys.modules[auth_module]
-    else:
-        root_module = __import__(
-            "auth.%s" % auth_type, globals=globals(), level=2)
-        module = getattr(root_module, auth_type)
-    # Override auth.is_authenticated
-    sys.modules[__name__].is_authenticated = module.is_authenticated
-    return module
-
-
-def is_authenticated(user, password):
-    """Check if the user is authenticated.
-
-    This method is overriden if an auth module is loaded.
-
-    """
-    return True  # Default is always True: no authentication

+ 0 - 61
radicale/auth/courier.py

@@ -1,61 +0,0 @@
-# This file is part of Radicale Server - Calendar Server
-# Copyright © 2011 Henry-Nicolas Tourneur
-#
-# 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 <http://www.gnu.org/licenses/>.
-
-"""
-Courier-Authdaemon authentication.
-
-"""
-
-import sys
-import socket
-
-from .. import config, log
-
-
-COURIER_SOCKET = config.get("auth", "courier_socket")
-
-
-def is_authenticated(user, password):
-    """Check if ``user``/``password`` couple is valid."""
-    if not user or not password:
-        return False
-
-    line = "%s\nlogin\n%s\n%s" % (sys.argv[0], user, password)
-    line = "AUTH %i\n%s" % (len(line), line)
-    try:
-        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-        sock.connect(COURIER_SOCKET)
-        log.LOGGER.debug("Sending to Courier socket the request: %s" % line)
-        sock.send(line)
-        data = sock.recv(1024)
-        sock.close()
-    except socket.error as exception:
-        log.LOGGER.debug(
-            "Unable to communicate with Courier socket: %s" % exception)
-        return False
-
-    log.LOGGER.debug("Got Courier socket response: %r" % data)
-
-    # Address, HOME, GID, and either UID or USERNAME are mandatory in resposne
-    # see http://www.courier-mta.org/authlib/README_authlib.html#authpipeproto
-    for line in data.split():
-        if "GID" in line:
-            return True
-
-    # default is reject
-    # this alleviates the problem of a possibly empty reply from authlib
-    # see http://www.courier-mta.org/authlib/README_authlib.html#authpipeproto
-    return False

+ 0 - 41
radicale/auth/http.py

@@ -1,41 +0,0 @@
-# This file is part of Radicale Server - Calendar Server
-# Copyright © 2012 Ehsanul Hoque
-# Copyright © 2013 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 <http://www.gnu.org/licenses/>.
-
-"""
-HTTP authentication.
-
-Authentication based on the ``requests`` module.
-
-Post a request to an authentication server with the username/password.
-Anything other than a 200/201 response is considered auth failure.
-
-"""
-
-import requests
-
-from .. import config, log
-
-AUTH_URL = config.get("auth", "http_url")
-USER_PARAM = config.get("auth", "http_user_parameter")
-PASSWORD_PARAM = config.get("auth", "http_password_parameter")
-
-
-def is_authenticated(user, password):
-    """Check if ``user``/``password`` couple is valid."""
-    log.LOGGER.debug("HTTP-based auth on %s." % AUTH_URL)
-    payload = {USER_PARAM: user, PASSWORD_PARAM: password}
-    return requests.post(AUTH_URL, data=payload).status_code in (200, 201)

+ 0 - 29
radicale/auth/remote_user.py

@@ -1,29 +0,0 @@
-# This file is part of Radicale Server - Calendar Server
-# Copyright © 2012 Ehsanul Hoque
-# Copyright © 2013 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 <http://www.gnu.org/licenses/>.
-
-"""
-Trusting the HTTP server auth mechanism.
-
-"""
-
-from .. import log
-
-
-def is_authenticated(user, password):
-    """Check if ``user`` is defined and assuming it's valid."""
-    log.LOGGER.debug("Got user %r from HTTP server." % user)
-    return user is not None

+ 5 - 26
radicale/config.py

@@ -45,43 +45,22 @@ INITIAL_CONFIG = {
         "can_skip_base_prefix": "False",
         "realm": "Radicale - Password Required"},
     "well-known": {
-         "caldav": "/%(user)s/caldav/",
-         "carddav": "/%(user)s/carddav/"},
+         "caldav": "/caldav/",
+         "carddav": "/carddav/"},
     "encoding": {
         "request": "utf-8",
         "stock": "utf-8"},
     "auth": {
         "type": "None",
-        "custom_handler": "",
         "htpasswd_filename": "/etc/radicale/users",
-        "htpasswd_encryption": "crypt",
-        "imap_hostname": "localhost",
-        "imap_port": "143",
-        "imap_ssl": "False",
-        "ldap_url": "ldap://localhost:389/",
-        "ldap_base": "ou=users,dc=example,dc=com",
-        "ldap_attribute": "uid",
-        "ldap_filter": "",
-        "ldap_binddn": "",
-        "ldap_password": "",
-        "ldap_scope": "OneLevel",
-        "pam_group_membership": "",
-        "courier_socket": "",
-        "http_url": "",
-        "http_user_parameter": "",
-        "http_password_parameter": ""},
-    "git": {
-        "committer": "Radicale <radicale@example.com>"},
+        "htpasswd_encryption": "crypt"},
     "rights": {
         "type": "None",
-        "custom_handler": "",
         "file": "~/.config/radicale/rights"},
     "storage": {
-        "type": "filesystem",
-        "custom_handler": "",
+        "type": "multifilesystem",
         "filesystem_folder": os.path.expanduser(
-            "~/.config/radicale/collections"),
-        "database_url": ""},
+            "~/.config/radicale/collections")},
     "logging": {
         "config": "/etc/radicale/logging",
         "debug": "False",

+ 1 - 2
radicale/pathutils.py

@@ -49,8 +49,7 @@ def is_safe_path_component(path):
     """
     if not path:
         return False
-    head, _ = posixpath.split(path)
-    if head:
+    if posixpath.split(path)[0]:
         return False
     if path in (".", ".."):
         return False

+ 23 - 15
radicale/rights/regex.py → radicale/rights.py

@@ -1,7 +1,5 @@
 # This file is part of Radicale Server - Calendar Server
-# Copyright © 2008 Nicolas Kandel
-# Copyright © 2008 Pascal Halter
-# Copyright © 2008-2016 Guillaume Ayoub
+# Copyright © 2012-2016 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
@@ -17,10 +15,13 @@
 # along with Radicale.  If not, see <http://www.gnu.org/licenses/>.
 
 """
-Rights management.
+Rights backends.
 
-Rights are based on a regex-based file whose name is specified in the config
-(section "right", key "file").
+This module loads the rights backend, according to the rights
+configuration.
+
+Default rights are based on a regex-based file whose name is specified in the
+config (section "right", key "file").
 
 Authentication login is matched against the "user" key, and collection's path
 is matched against the "collection" key. You can use Python's ConfigParser
@@ -36,19 +37,26 @@ Leading or ending slashes are trimmed from collection's path.
 
 """
 
+import os.path
 import re
 import sys
-import os.path
+from configparser import ConfigParser
+from io import StringIO
 
-from .. import config, log
+from . import config, log
 
-# Manage Python2/3 different modules
-if sys.version_info[0] == 2:
-    from ConfigParser import ConfigParser
-    from StringIO import StringIO
-else:
-    from configparser import ConfigParser
-    from io import StringIO
+
+def _load():
+    """Load the rights manager chosen in configuration."""
+    rights_type = config.get("rights", "type")
+    if rights_type == "None":
+        sys.modules[__name__].authorized = (
+            lambda user, collection, permission: True)
+    elif rights_type in DEFINED_RIGHTS or rights_type == "from_file":
+        pass  # authorized is already defined
+    else:
+        __import__(rights_type)
+        sys.modules[__name__].authorized = sys.modules[rights_type].authorized
 
 
 DEFINED_RIGHTS = {

+ 0 - 50
radicale/rights/__init__.py

@@ -1,50 +0,0 @@
-# This file is part of Radicale Server - Calendar Server
-# Copyright © 2012-2016 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 <http://www.gnu.org/licenses/>.
-
-"""
-Rights backends.
-
-This module loads the rights backend, according to the rights
-configuration.
-
-"""
-
-import sys
-
-from .. import config
-
-
-def load():
-    """Load list of available storage managers."""
-    storage_type = config.get("rights", "type")
-    if storage_type == "custom":
-        rights_module = config.get("rights", "custom_handler")
-        __import__(rights_module)
-        module = sys.modules[rights_module]
-    else:
-        root_module = __import__("rights.regex", globals=globals(), level=2)
-        module = root_module.regex
-    sys.modules[__name__].authorized = module.authorized
-    return module
-
-
-def authorized(user, collection, right):
-    """Check that an user has rights on a  collection.
-
-    This method is overriden when the appropriate rights backend is loaded.
-
-    """
-    raise NotImplementedError()

+ 82 - 31
radicale/storage/multifilesystem.py → radicale/storage.py

@@ -1,6 +1,6 @@
 # This file is part of Radicale Server - Calendar Server
 # Copyright © 2014 Jean-Marc Martins
-# Copyright © 2014-2016 Guillaume Ayoub
+# Copyright © 2012-2016 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
@@ -16,49 +16,87 @@
 # along with Radicale.  If not, see <http://www.gnu.org/licenses/>.
 
 """
-Multi files per calendar filesystem storage backend.
+Storage backends.
+
+This module loads the storage backend, according to the storage
+configuration.
+
+Default storage uses one folder per collection and one file per collection
+entry.
 
 """
 
-import os
 import json
+import os
+import posixpath
 import shutil
-import time
 import sys
+import time
 from contextlib import contextmanager
 
-from . import filesystem
-from .. import ical
-from .. import log
-from .. import pathutils
+from . import config, ical, log, pathutils
+
+
+def _load():
+    """Load the storage manager chosen in configuration."""
+    storage_type = config.get("storage", "type")
+    if storage_type == "multifilesystem":
+        module = sys.modules[__name__]
+    else:
+        __import__(storage_type)
+        module = sys.modules[storage_type]
+    ical.Collection = module.Collection
+
 
+FOLDER = os.path.expanduser(config.get("storage", "filesystem_folder"))
+FILESYSTEM_ENCODING = sys.getfilesystemencoding()
 
-class Collection(filesystem.Collection):
+
+@contextmanager
+def _open(path, mode="r"):
+    """Open a file at ``path`` with encoding set in the configuration."""
+    abs_path = os.path.join(FOLDER, path.replace("/", os.sep))
+    with open(abs_path, mode, encoding=config.get("encoding", "stock")) as fd:
+        yield fd
+
+
+class Collection(ical.Collection):
     """Collection stored in several files per calendar."""
+    @property
+    def _filesystem_path(self):
+        """Absolute path of the file at local ``path``."""
+        return pathutils.path_to_filesystem(self.path, FOLDER)
+
+    @property
+    def _props_path(self):
+        """Absolute path of the file storing the collection properties."""
+        return self._filesystem_path + ".props"
+
     def _create_dirs(self):
+        """Create folder storing the collection if absent."""
         if not os.path.exists(self._filesystem_path):
             os.makedirs(self._filesystem_path)
 
-    @property
-    def headers(self):
-        return (
-            ical.Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"),
-            ical.Header("VERSION:%s" % self.version))
-
-    def write(self):
+    def save(self, text):
         self._create_dirs()
-        for component in self.components:
-            text = ical.serialize(
-                self.tag, self.headers, [component] + self.timezones)
-            name = component.name
+        item_types = (
+            ical.Timezone, ical.Event, ical.Todo, ical.Journal, ical.Card)
+        for name, component in self._parse(text, item_types).items():
             if not pathutils.is_safe_filesystem_path_component(name):
+                # TODO: Timezones with slashes can't be saved
                 log.LOGGER.debug(
                     "Can't tranlate name safely to filesystem, "
                     "skipping component: %s", name)
                 continue
-            filesystem_path = os.path.join(self._filesystem_path, name)
-            with filesystem.open(filesystem_path, "w") as fd:
-                fd.write(text)
+            filename = os.path.join(self._filesystem_path, name)
+            with _open(filename, "w") as fd:
+                fd.write(component.text)
+
+    @property
+    def headers(self):
+        return (
+            ical.Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"),
+            ical.Header("VERSION:%s" % self.version))
 
     def delete(self):
         shutil.rmtree(self._filesystem_path)
@@ -92,7 +130,7 @@ class Collection(filesystem.Collection):
         for filename in filenames:
             path = os.path.join(self._filesystem_path, filename)
             try:
-                with filesystem.open(path) as fd:
+                with _open(path) as fd:
                     items.update(self._parse(fd.read(), components))
             except (OSError, IOError) as e:
                 log.LOGGER.warning(
@@ -101,16 +139,30 @@ class Collection(filesystem.Collection):
         return ical.serialize(
             self.tag, self.headers, sorted(items.values(), key=lambda x: x.name))
 
+    @classmethod
+    def children(cls, path):
+        filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
+        _, directories, files = next(os.walk(filesystem_path))
+        for filename in directories + files:
+            # make sure that the local filename can be translated
+            # into an internal path
+            if not pathutils.is_safe_path_component(filename):
+                log.LOGGER.debug("Skipping unsupported filename: %s", filename)
+                continue
+            rel_filename = posixpath.join(path, filename)
+            if cls.is_node(rel_filename) or cls.is_leaf(rel_filename):
+                yield cls(rel_filename)
+
     @classmethod
     def is_node(cls, path):
-        filesystem_path = pathutils.path_to_filesystem(path, filesystem.FOLDER)
+        filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
         return (
             os.path.isdir(filesystem_path) and
             not os.path.exists(filesystem_path + ".props"))
 
     @classmethod
     def is_leaf(cls, path):
-        filesystem_path = pathutils.path_to_filesystem(path, filesystem.FOLDER)
+        filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
         return (
             os.path.isdir(filesystem_path) and os.path.exists(path + ".props"))
 
@@ -132,8 +184,7 @@ class Collection(filesystem.Collection):
         old_properties = properties.copy()
         yield properties
         # On exit
-        if os.path.exists(self._props_path):
-          self._create_dirs()
-          if old_properties != properties:
-              with open(self._props_path, "w") as prop_file:
-                  json.dump(properties, prop_file)
+        self._create_dirs()
+        if old_properties != properties:
+            with open(self._props_path, "w") as prop_file:
+                json.dump(properties, prop_file)

+ 0 - 42
radicale/storage/__init__.py

@@ -1,42 +0,0 @@
-# This file is part of Radicale Server - Calendar Server
-# Copyright © 2012-2016 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 <http://www.gnu.org/licenses/>.
-
-"""
-Storage backends.
-
-This module loads the storage backend, according to the storage
-configuration.
-
-"""
-
-import sys
-
-from .. import config, ical
-
-
-def load():
-    """Load list of available storage managers."""
-    storage_type = config.get("storage", "type")
-    if storage_type == "custom":
-        storage_module = config.get("storage", "custom_handler")
-        __import__(storage_module)
-        module = sys.modules[storage_module]
-    else:
-        root_module = __import__(
-            "storage.%s" % storage_type, globals=globals(), level=2)
-        module = getattr(root_module, storage_type)
-    ical.Collection = module.Collection
-    return module

+ 0 - 282
radicale/storage/database.py

@@ -1,282 +0,0 @@
-# This file is part of Radicale Server - Calendar Server
-# Copyright © 2013 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 <http://www.gnu.org/licenses/>.
-
-"""
-SQLAlchemy storage backend.
-
-"""
-
-import time
-from datetime import datetime
-from contextlib import contextmanager
-
-from sqlalchemy import create_engine, Column, Unicode, Integer, ForeignKey
-from sqlalchemy import func
-from sqlalchemy.orm import sessionmaker, relationship
-from sqlalchemy.ext.declarative import declarative_base
-
-from .. import config, ical
-
-
-# These are classes, not constants
-# pylint: disable=C0103
-Base = declarative_base()
-Session = sessionmaker()
-Session.configure(bind=create_engine(config.get("storage", "database_url")))
-# pylint: enable=C0103
-
-
-class DBCollection(Base):
-    """Table of collections."""
-    __tablename__ = "collection"
-
-    path = Column(Unicode, primary_key=True)
-    parent_path = Column(Unicode, ForeignKey("collection.path"))
-
-    parent = relationship(
-        "DBCollection", backref="children", remote_side=[path])
-
-
-class DBItem(Base):
-    """Table of collection's items."""
-    __tablename__ = "item"
-
-    name = Column(Unicode, primary_key=True)
-    tag = Column(Unicode)
-    collection_path = Column(Unicode, ForeignKey("collection.path"))
-
-    collection = relationship("DBCollection", backref="items")
-
-
-class DBHeader(Base):
-    """Table of item's headers."""
-    __tablename__ = "header"
-
-    name = Column(Unicode, primary_key=True)
-    value = Column(Unicode)
-    collection_path = Column(
-        Unicode, ForeignKey("collection.path"), primary_key=True)
-
-    collection = relationship("DBCollection", backref="headers")
-
-
-class DBLine(Base):
-    """Table of item's lines."""
-    __tablename__ = "line"
-
-    name = Column(Unicode)
-    value = Column(Unicode)
-    item_name = Column(Unicode, ForeignKey("item.name"))
-    timestamp = Column(
-        Integer, default=lambda: time.time() * 10 ** 6, primary_key=True)
-
-    item = relationship("DBItem", backref="lines", order_by=timestamp)
-
-
-class DBProperty(Base):
-    """Table of collection's properties."""
-    __tablename__ = "property"
-
-    name = Column(Unicode, primary_key=True)
-    value = Column(Unicode)
-    collection_path = Column(
-        Unicode, ForeignKey("collection.path"), primary_key=True)
-
-    collection = relationship(
-        "DBCollection", backref="properties", cascade="delete")
-
-
-class Collection(ical.Collection):
-    """Collection stored in a database."""
-    def __init__(self, path, principal=False):
-        self.session = Session()
-        super().__init__(path, principal)
-
-    def __del__(self):
-        self.session.commit()
-
-    def _query(self, item_types):
-        """Get collection's items matching ``item_types``."""
-        item_objects = []
-        for item_type in item_types:
-            items = (
-                self.session.query(DBItem)
-                .filter_by(collection_path=self.path, tag=item_type.tag)
-                .order_by(DBItem.name).all())
-            for item in items:
-                text = "\n".join(
-                    "%s:%s" % (line.name, line.value) for line in item.lines)
-                item_objects.append(item_type(text, item.name))
-        return item_objects
-
-    @property
-    def _modification_time(self):
-        """Collection's last modification time."""
-        timestamp = (
-            self.session.query(func.max(DBLine.timestamp))
-            .join(DBItem).filter_by(collection_path=self.path).first()[0])
-        if timestamp:
-            return datetime.fromtimestamp(float(timestamp) / 10 ** 6)
-        else:
-            return datetime.now()
-
-    @property
-    def _db_collection(self):
-        """Collection's object mapped to the table line."""
-        return self.session.query(DBCollection).get(self.path)
-
-    def write(self):
-        if self._db_collection:
-            for item in self._db_collection.items:
-                for line in item.lines:
-                    self.session.delete(line)
-                self.session.delete(item)
-            for header in self._db_collection.headers:
-                self.session.delete(header)
-        else:
-            db_collection = DBCollection()
-            db_collection.path = self.path
-            db_collection.parent_path = "/".join(self.path.split("/")[:-1])
-            self.session.add(db_collection)
-
-        for header in self.headers:
-            db_header = DBHeader()
-            db_header.name, db_header.value = header.text.split(":", 1)
-            db_header.collection_path = self.path
-            self.session.add(db_header)
-
-        for item in self.items.values():
-            db_item = DBItem()
-            db_item.name = item.name
-            db_item.tag = item.tag
-            db_item.collection_path = self.path
-            self.session.add(db_item)
-
-            for line in ical.unfold(item.text):
-                db_line = DBLine()
-                db_line.name, db_line.value = line.split(":", 1)
-                db_line.item_name = item.name
-                self.session.add(db_line)
-
-    def delete(self):
-        self.session.delete(self._db_collection)
-
-    @property
-    def text(self):
-        return ical.serialize(self.tag, self.headers, self.components)
-
-    @property
-    def etag(self):
-        return '"%s"' % hash(self._modification_time)
-
-    @property
-    def headers(self):
-        headers = (
-            self.session.query(DBHeader)
-            .filter_by(collection_path=self.path)
-            .order_by(DBHeader.name).all())
-        return [
-            ical.Header("%s:%s" % (header.name, header.value))
-            for header in headers]
-
-    @classmethod
-    def children(cls, path):
-        session = Session()
-        children = (
-            session.query(DBCollection)
-            .filter_by(parent_path=path or "").all())
-        collections = [cls(child.path) for child in children]
-        session.close()
-        return collections
-
-    @classmethod
-    def is_node(cls, path):
-        if not path:
-            return True
-        session = Session()
-        result = (
-            session.query(DBCollection)
-            .filter_by(parent_path=path or "").count() > 0)
-        session.close()
-        return result
-
-    @classmethod
-    def is_leaf(cls, path):
-        if not path:
-            return False
-        session = Session()
-        result = (
-            session.query(DBItem)
-            .filter_by(collection_path=path or "").count() > 0)
-        session.close()
-        return result
-
-    @property
-    def last_modified(self):
-        return time.strftime(
-            "%a, %d %b %Y %H:%M:%S +0000", self._modification_time.timetuple())
-
-    @property
-    @contextmanager
-    def props(self):
-        # On enter
-        properties = {}
-        db_properties = (
-            self.session.query(DBProperty)
-            .filter_by(collection_path=self.path).all())
-        for prop in db_properties:
-            properties[prop.name] = prop.value
-        old_properties = properties.copy()
-        yield properties
-        # On exit
-        if old_properties != properties:
-            for prop in db_properties:
-                self.session.delete(prop)
-            for name, value in properties.items():
-                prop = DBProperty(name=name, value=value or '',
-                                  collection_path=self.path)
-                self.session.add(prop)
-
-    @property
-    def components(self):
-        return self._query((ical.Event, ical.Todo, ical.Journal, ical.Card))
-
-    @property
-    def events(self):
-        return self._query((ical.Event,))
-
-    @property
-    def todos(self):
-        return self._query((ical.Todo,))
-
-    @property
-    def journals(self):
-        return self._query((ical.Journal,))
-
-    @property
-    def timezones(self):
-        return self._query((ical.Timezone,))
-
-    @property
-    def cards(self):
-        return self._query((ical.Card,))
-
-    def save(self):
-        """Save the text into the collection.
-
-        This method is not used for databases.
-
-        """

+ 0 - 142
radicale/storage/filesystem.py

@@ -1,142 +0,0 @@
-# This file is part of Radicale Server - Calendar Server
-# Copyright © 2012-2016 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 <http://www.gnu.org/licenses/>.
-
-"""
-Filesystem storage backend.
-
-"""
-
-import codecs
-import os
-import posixpath
-import json
-import time
-import sys
-from contextlib import contextmanager
-
-from .. import config, ical, log, pathutils
-
-
-FOLDER = os.path.expanduser(config.get("storage", "filesystem_folder"))
-FILESYSTEM_ENCODING = sys.getfilesystemencoding()
-
-try:
-    from dulwich.repo import Repo
-    GIT_REPOSITORY = Repo(FOLDER)
-except:
-    GIT_REPOSITORY = None
-
-
-# This function overrides the builtin ``open`` function for this module
-# pylint: disable=W0622
-@contextmanager
-def open(path, mode="r"):
-    """Open a file at ``path`` with encoding set in the configuration."""
-    # On enter
-    abs_path = os.path.join(FOLDER, path.replace("/", os.sep))
-    with codecs.open(abs_path, mode, config.get("encoding", "stock")) as fd:
-        yield fd
-    # On exit
-    if GIT_REPOSITORY and mode == "w":
-        path = os.path.relpath(abs_path, FOLDER)
-        GIT_REPOSITORY.stage([path])
-        committer = config.get("git", "committer")
-        GIT_REPOSITORY.do_commit(
-            path.encode("utf-8"), committer=committer.encode("utf-8"))
-# pylint: enable=W0622
-
-
-class Collection(ical.Collection):
-    """Collection stored in a flat ical file."""
-    @property
-    def _filesystem_path(self):
-        """Absolute path of the file at local ``path``."""
-        return pathutils.path_to_filesystem(self.path, FOLDER)
-
-    @property
-    def _props_path(self):
-        """Absolute path of the file storing the collection properties."""
-        return self._filesystem_path + ".props"
-
-    def _create_dirs(self):
-        """Create folder storing the collection if absent."""
-        if not os.path.exists(os.path.dirname(self._filesystem_path)):
-            os.makedirs(os.path.dirname(self._filesystem_path))
-
-    def save(self, text):
-        self._create_dirs()
-        with open(self._filesystem_path, "w") as fd:
-            fd.write(text)
-
-    def delete(self):
-        os.remove(self._filesystem_path)
-        os.remove(self._props_path)
-
-    @property
-    def text(self):
-        try:
-            with open(self._filesystem_path) as fd:
-                return fd.read()
-        except IOError:
-            return ""
-
-    @classmethod
-    def children(cls, path):
-        filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
-        _, directories, files = next(os.walk(filesystem_path))
-        for filename in directories + files:
-            # make sure that the local filename can be translated
-            # into an internal path
-            if not pathutils.is_safe_path_component(filename):
-                log.LOGGER.debug("Skipping unsupported filename: %s", filename)
-                continue
-            rel_filename = posixpath.join(path, filename)
-            if cls.is_node(rel_filename) or cls.is_leaf(rel_filename):
-                yield cls(rel_filename)
-
-    @classmethod
-    def is_node(cls, path):
-        filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
-        return os.path.isdir(filesystem_path)
-
-    @classmethod
-    def is_leaf(cls, path):
-        filesystem_path = pathutils.path_to_filesystem(path, FOLDER)
-        return (
-            os.path.isfile(filesystem_path) and not
-            filesystem_path.endswith(".props"))
-
-    @property
-    def last_modified(self):
-        modification_time = time.gmtime(
-            os.path.getmtime(self._filesystem_path))
-        return time.strftime("%a, %d %b %Y %H:%M:%S +0000", modification_time)
-
-    @property
-    @contextmanager
-    def props(self):
-        # On enter
-        properties = {}
-        if os.path.exists(self._props_path):
-            with open(self._props_path) as prop_file:
-                properties.update(json.load(prop_file))
-        old_properties = properties.copy()
-        yield properties
-        # On exit
-        self._create_dirs()
-        if old_properties != properties:
-            with open(self._props_path, "w") as prop_file:
-                json.dump(properties, prop_file)