diff --git a/config b/config index 453a45e1..086110cd 100644 --- a/config +++ b/config @@ -96,13 +96,19 @@ http_password_parameter = [rights] # Rights management method -# Value: None | owner_only | owner_write | from_file +# Value: None | owner_only | owner_write | from_file | regex type = None # File for rights management from_file file = ~/.config/radicale/rights +# File for rights management regex +regex_file =~/.config/radicale/regex +# use this as alternative method +regex_secondary = owner_only + + [storage] # Storage backend # Value: filesystem | database diff --git a/radicale/config.py b/radicale/config.py index 45b758fd..81db5461 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -74,7 +74,9 @@ INITIAL_CONFIG = { "http_password_parameter": ""}, "rights": { "type": "None", - "file": ""}, + "file": "", + "regex_file": "", + "regex_secondary": "owner_only"}, "storage": { "type": "filesystem", "filesystem_folder": os.path.expanduser( diff --git a/radicale/rights/regex.py b/radicale/rights/regex.py new file mode 100644 index 00000000..a886fb68 --- /dev/null +++ b/radicale/rights/regex.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2012-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 . + +""" +Regex-based rights. + +You can define an secondary rights management method. If not, it will use +"owner_only", which implies the owners have all rights on their collections. +Secondary rights management method is specified in the config (section +"right", key "regex_secondary"). + +Regexes are read from a file whose name is specified in the config (section +"right", key "regex_file"). + +Test string for regex is "user|collection" per default, because "|" is +not allowed in an URL. You may set this string per rule, using Python's +ConfigParser interpolation: %(user)s and %(collection)s +In fact you may also set user/collection to a fixed value per rule. But you +should consider using a secondary rights management method (e.g. "from_file"). + + +Section names are only used for naming the rule. +Leading or Ending "/"s are trimmed from collection names. + +Example: + +# This means all users starting with "admin" may read any collection +[admin] +regex: ^admin.*\|.+?$ +permission: r + +# This means all users may read and write any collection starting with public. +# We do so by just not testing against the user string. +[public] +glue: %(collection)s +regex: ^public(/.+)?$ +permission: rw + +# A little more complex: +# Give read access to users from a domain for all collections of all the users: +[domain-wide-access] +regex: ^.+@(.+)\|.+@\1(/.+)?$ +permission: r + + +""" + +import os.path +import re + +from radicale import config, log, rights +# Manage Python2/3 different modules +# pylint: disable=F0401 +try: + from configparser import ( + ConfigParser as ConfigParser, NoSectionError, NoOptionError) +except ImportError: + from ConfigParser import ( + ConfigParser as ConfigParser, NoSectionError, NoOptionError) +# pylint: enable=F0401 + + +FILENAME = ( + os.path.expanduser(config.get("rights", "regex_file")) or + log.LOGGER.error("No file name configured for rights type 'regex'")) + + +def _read_regex(user, collection): + """Load regex from file.""" + regex = ConfigParser({'user': user, 'collection': collection}) + if not regex.read(FILENAME): + log.LOGGER.error( + "File '%s' not found for rights management type 'regex'" % FILENAME) + return regex + +def _read_from_sections(user, collection, perm): + """Get regex sections.""" + regex = _read_regex(user, collection) + try: + for section in regex.sections(): + if _matches_section(user, collection, section): + if perm in regex.get(section, "permission"): + return True + except (NoSectionError, NoOptionError): + return False + return False + +def _matches_section(user, collection, section): + """Regex section against user and collection""" + log.LOGGER.debug("Reading regex from file %s" % FILENAME) + regex = _read_regex(user, collection) + log.LOGGER.debug("Match against section '%s'" % section) + + try: + test = regex.get(section, 'glue') + except (NoOptionError): + test = user+'|'+collection; + + try: + match = re.match(regex.get(section, 'regex'), test) + if match: + return True; + log.LOGGER.debug("Test-String '%s' does not match against '%s' from section '%s'" % \ + (test, regex.get(section, 'regex'), section)) + except (NoSectionError, NoOptionError): + return False + return False + + + +def _get_secondary(): + """Get secondary rights management method""" + try: + secondary = config.get("rights", "regex_secondary") + if not secondary or secondary == None: + secondary = 'owner_only' + + root_module = __import__( + "rights.%s" % secondary, globals=globals(), level=2) + module = getattr(root_module, secondary) + return module + except (ImportError, NoSectionError, NoOptionError): + return None + + +def read_authorized(user, collection): + """Check if the user is allowed to read the collection.""" + if user is None: + return False + elif _get_secondary() != None and _get_secondary().read_authorized(user, collection): + return True + else: + return _read_from_sections( + user, collection.url.rstrip("/") or "/", "r") + + +def write_authorized(user, collection): + """Check if the user is allowed to write the collection.""" + if user is None: + return False + elif _get_secondary() != None and _get_secondary().write_authorized(user, collection): + return True + else: + return _read_from_sections( + user, collection.url.rstrip("/") or "/", "w")