From a725031bd6416449084d568adc578eb4e3d7d064 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Mon, 2 Sep 2013 12:01:28 +0200 Subject: [PATCH 1/9] Big changes: next version is 0.9 --- NEWS.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index 4b7d9f4..bb929c8 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -3,8 +3,8 @@ ====== -0.8.1 - *Not released yet* -========================== +0.9 - *Not released yet* +======================== * 1-file-per-event storage (by Jean-Marc Martins) * Git support for filesystem storages (by Jean-Marc Martins) From ff535b62b07413818fdda52d94f5c5a61d663b7c Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 6 Sep 2013 16:00:12 +0200 Subject: [PATCH 2/9] Remove useless config keys --- config | 7 +------ radicale/config.py | 2 -- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/config b/config index 75b12bd..5746523 100644 --- a/config +++ b/config @@ -30,7 +30,7 @@ dns_lookup = True # Root URL of Radicale (starting and ending with a slash) base_prefix = / # Message displayed in the client when a password is needed -realm = Radicale - Password Required lol +realm = Radicale - Password Required [encoding] @@ -45,11 +45,6 @@ stock = utf-8 # Value: None | htpasswd | IMAP | LDAP | PAM | courier | http type = None -# Usernames used for public collections, separated by a comma -public_users = public -# Usernames used for private collections, separated by a comma -private_users = private - # Htpasswd filename htpasswd_filename = /etc/radicale/users # Htpasswd encryption method diff --git a/radicale/config.py b/radicale/config.py index 44d057e..12c8408 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -53,8 +53,6 @@ INITIAL_CONFIG = { "stock": "utf-8"}, "auth": { "type": "None", - "public_users": "public", - "private_users": "private", "htpasswd_filename": "/etc/radicale/users", "htpasswd_encryption": "crypt", "imap_hostname": "localhost", From 0e5ef007f1bde12e7317ba0a4cc06f303c542ebf Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 6 Sep 2013 22:09:16 +0200 Subject: [PATCH 3/9] Use table attribute instead of string in database order by (probably fixes #59) --- radicale/storage/database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radicale/storage/database.py b/radicale/storage/database.py index f42e75a..a23bf9a 100644 --- a/radicale/storage/database.py +++ b/radicale/storage/database.py @@ -116,7 +116,7 @@ class Collection(ical.Collection): items = ( self.session.query(DBItem) .filter_by(collection_path=self.path, tag=item_type.tag) - .order_by("name").all()) + .order_by(DBItem.name).all()) for item in items: text = "\n".join( "%s:%s" % (line.key, line.value) for line in item.lines) @@ -189,7 +189,7 @@ class Collection(ical.Collection): headers = ( self.session.query(DBHeader) .filter_by(collection_path=self.path) - .order_by("key").all()) + .order_by(DBHeader.key).all()) return [ ical.Header("%s:%s" % (header.key, header.value)) for header in headers] From 15d8a8eb84fdea1fcace28973c706883f3050c3c Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sat, 7 Sep 2013 09:50:13 +0200 Subject: [PATCH 4/9] Remove useless primary key from line table --- radicale/storage/database.py | 6 +++--- schema.sql | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/radicale/storage/database.py b/radicale/storage/database.py index a23bf9a..0e40b0a 100644 --- a/radicale/storage/database.py +++ b/radicale/storage/database.py @@ -78,10 +78,10 @@ class DBLine(Base): """Table of item's lines.""" __tablename__ = "line" - key = Column(String, primary_key=True) + key = Column(String) value = Column(String) - item_name = Column(String, ForeignKey("item.name"), primary_key=True) - timestamp = Column(DateTime, default=datetime.now) + item_name = Column(String, ForeignKey("item.name")) + timestamp = Column(DateTime, default=datetime.now, primary_key=True) item = relationship( "DBItem", backref="lines", order_by=timestamp) diff --git a/schema.sql b/schema.sql index c748e5f..b6bb828 100644 --- a/schema.sql +++ b/schema.sql @@ -19,8 +19,7 @@ create table line ( key varchar not null, value varchar not null, item_name varchar references item (name) not null, - timestamp timestamp not null, - primary key (key, value, item_name, timestamp)); + timestamp timestamp not null); create table property ( key varchar not null, From 33f09016fdb43351a0d2908098f6ef4a9763ceaf Mon Sep 17 00:00:00 2001 From: Jean-Marc Martins Date: Mon, 9 Sep 2013 15:36:30 +0200 Subject: [PATCH 5/9] Fixes tests to use the default project root config --- radicale/config.py | 1 + tests/__init__.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/radicale/config.py b/radicale/config.py index 44d057e..47f6544 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -99,4 +99,5 @@ if "RADICALE_CONFIG" in os.environ: _CONFIG_PARSER.read(os.environ["RADICALE_CONFIG"]) # Wrap config module into ConfigParser instance +_CONFIG_PARSER.INITIAL_CONFIG = INITIAL_CONFIG sys.modules[__name__] = _CONFIG_PARSER diff --git a/tests/__init__.py b/tests/__init__.py index ec5a84d..14ef5f6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -33,6 +33,11 @@ from io import BytesIO sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) import radicale + +RADICALE_CONFIG = os.path.join(os.path.dirname(os.path.dirname(__file__)), + "config") +os.environ["RADICALE_CONFIG"] = RADICALE_CONFIG + from radicale import config from radicale.auth import htpasswd from radicale.storage import filesystem, database @@ -45,6 +50,12 @@ class BaseTest(object): """Base class for tests.""" def request(self, method, path, data=None, **args): """Send a request.""" + # Create a ConfigParser and configure it + # This uses the default config file at the root of the + # Radicale Project + config.read(os.environ.get("RADICALE_CONFIG")) + self._CONFIG_PARSER = config + self.application._status = None self.application._headers = None self.application._answer = None @@ -69,6 +80,10 @@ class BaseTest(object): self.application._status = status self.application._headers = headers + @property + def config_instance(self): + return self._CONFIG_PARSER + class FileSystem(BaseTest): """Base class for filesystem tests.""" From d8f686a497aefb7b9f5c7dc16fd54a06054721e0 Mon Sep 17 00:00:00 2001 From: Jean-Marc Martins Date: Mon, 9 Sep 2013 17:00:04 +0200 Subject: [PATCH 6/9] Fixed stupid redundancy in tests --- radicale/config.py | 1 - tests/__init__.py | 15 ++------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/radicale/config.py b/radicale/config.py index 47f6544..44d057e 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -99,5 +99,4 @@ if "RADICALE_CONFIG" in os.environ: _CONFIG_PARSER.read(os.environ["RADICALE_CONFIG"]) # Wrap config module into ConfigParser instance -_CONFIG_PARSER.INITIAL_CONFIG = INITIAL_CONFIG sys.modules[__name__] = _CONFIG_PARSER diff --git a/tests/__init__.py b/tests/__init__.py index 14ef5f6..b5bd194 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -34,9 +34,8 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) import radicale -RADICALE_CONFIG = os.path.join(os.path.dirname(os.path.dirname(__file__)), - "config") -os.environ["RADICALE_CONFIG"] = RADICALE_CONFIG +os.environ["RADICALE_CONFIG"] = os.path.join(os.path.dirname( + os.path.dirname(__file__)), "config") from radicale import config from radicale.auth import htpasswd @@ -50,12 +49,6 @@ class BaseTest(object): """Base class for tests.""" def request(self, method, path, data=None, **args): """Send a request.""" - # Create a ConfigParser and configure it - # This uses the default config file at the root of the - # Radicale Project - config.read(os.environ.get("RADICALE_CONFIG")) - self._CONFIG_PARSER = config - self.application._status = None self.application._headers = None self.application._answer = None @@ -80,10 +73,6 @@ class BaseTest(object): self.application._status = status self.application._headers = headers - @property - def config_instance(self): - return self._CONFIG_PARSER - class FileSystem(BaseTest): """Base class for filesystem tests.""" From 43785e48a959c9e7b3300ba7fc663f41ee59ecde Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Thu, 12 Sep 2013 13:48:49 +0200 Subject: [PATCH 7/9] Get configuration keys at runtime, not when module is imported (fixes #64) --- radicale/__init__.py | 13 +++++-------- radicale/rights.py | 23 ++++++++++++----------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/radicale/__init__.py b/radicale/__init__.py index 80af1a4..c98ff03 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -55,13 +55,6 @@ VERSION = "git" # tries to access information they don't have rights to NOT_ALLOWED = (client.FORBIDDEN, {}, None) -# Standard "authenticate" response that is returned when a user tries to access -# non-public information w/o submitting proper authentication credentials -WRONG_CREDENTIALS = ( - client.UNAUTHORIZED, - {"WWW-Authenticate": "Basic realm=\"%s\"" % config.get("server", "realm")}, - None) - class HTTPServer(wsgiref.simple_server.WSGIServer, object): """HTTP server.""" @@ -303,7 +296,11 @@ class Application(object): else: # Unknown or unauthorized user log.LOGGER.info("%s refused" % (user or "Anonymous user")) - status, headers, answer = WRONG_CREDENTIALS + status = client.UNAUTHORIZED + headers = { + "WWW-Authenticate": + "Basic realm=\"%s\"" % config.get("server", "realm")} + answer = None # Set content length if answer: diff --git a/radicale/rights.py b/radicale/rights.py index 232fec2..88515bf 100644 --- a/radicale/rights.py +++ b/radicale/rights.py @@ -50,8 +50,6 @@ except ImportError: # pylint: enable=F0401 -FILENAME = os.path.expanduser(config.get("rights", "file")) -TYPE = config.get("rights", "type").lower() DEFINED_RIGHTS = { "owner_write": "[r]\nuser:.*\ncollection:.*\npermission:r\n" "[w]\nuser:.*\ncollection:^%(login)s/.+$\npermission:w", @@ -60,17 +58,19 @@ DEFINED_RIGHTS = { def _read_from_sections(user, collection, permission): """Get regex sections.""" + filename = os.path.expanduser(config.get("rights", "file")) + rights_type = config.get("rights", "type").lower() regex = ConfigParser({"login": user, "path": collection}) - if TYPE in DEFINED_RIGHTS: - log.LOGGER.debug("Rights type '%s'" % TYPE) - regex.readfp(io.BytesIO(DEFINED_RIGHTS[TYPE])) - elif TYPE == "from_file": - log.LOGGER.debug("Reading rights from file %s" % FILENAME) - if not regex.read(FILENAME): - log.LOGGER.error("File '%s' not found for rights" % FILENAME) + if rights_type in DEFINED_RIGHTS: + log.LOGGER.debug("Rights type '%s'" % rights_type) + regex.readfp(io.BytesIO(DEFINED_RIGHTS[rights_type])) + elif rights_type == "from_file": + log.LOGGER.debug("Reading rights from file %s" % filename) + if not regex.read(filename): + log.LOGGER.error("File '%s' not found for rights" % filename) return False else: - log.LOGGER.error("Unknown rights type '%s'" % TYPE) + log.LOGGER.error("Unknown rights type '%s'" % rights_type) return False for section in regex.sections(): @@ -92,5 +92,6 @@ def _read_from_sections(user, collection, permission): def authorized(user, collection, right): """Check if the user is allowed to read or write the collection.""" - return TYPE == "none" or (user and _read_from_sections( + rights_type = config.get("rights", "type").lower() + return rights_type == "none" or (user and _read_from_sections( user, collection.url.rstrip("/") or "/", right)) From 58faf725b08b355345a233396148dd7169df6b4a Mon Sep 17 00:00:00 2001 From: Jean-Marc Martins Date: Thu, 12 Sep 2013 17:39:20 +0200 Subject: [PATCH 8/9] Fixed authentication for anonymous users --- radicale/__init__.py | 10 +++++++++- radicale/rights.py | 6 ++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/radicale/__init__.py b/radicale/__init__.py index c98ff03..d2ca309 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -279,7 +279,7 @@ class Application(object): user = password = None if not items or function == self.options or \ - auth.is_authenticated(user, password): + auth.is_authenticated(user, password) if user else True: read_allowed_items, write_allowed_items = \ self.collect_allowed_items(items, user) @@ -290,6 +290,14 @@ class Application(object): status, headers, answer = function( environ, read_allowed_items, write_allowed_items, content, user) + elif not user: + # Unknown or unauthorized user + log.LOGGER.info("%s refused" % (user or "Anonymous user")) + status = client.UNAUTHORIZED + headers = { + "WWW-Authenticate": + "Basic realm=\"%s\"" % config.get("server", "realm")} + answer = None else: # Good user but has no rights to any of the given collections status, headers, answer = NOT_ALLOWED diff --git a/radicale/rights.py b/radicale/rights.py index 88515bf..e43c6dd 100644 --- a/radicale/rights.py +++ b/radicale/rights.py @@ -93,5 +93,7 @@ def _read_from_sections(user, collection, permission): def authorized(user, collection, right): """Check if the user is allowed to read or write the collection.""" rights_type = config.get("rights", "type").lower() - return rights_type == "none" or (user and _read_from_sections( - user, collection.url.rstrip("/") or "/", right)) + return rights_type == "none" or ( + (True if not user else user) and _read_from_sections( + user if user else "", collection.url.rstrip("/") or "/", right) + ) From e2512b12fb612a793e596ec96f054e083e10801f Mon Sep 17 00:00:00 2001 From: Jean-Marc Martins Date: Fri, 13 Sep 2013 15:05:02 +0200 Subject: [PATCH 9/9] Fixed partially anonymous authentication --- radicale/__init__.py | 35 +++++++++++++---------------------- radicale/rights.py | 11 ++++++----- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/radicale/__init__.py b/radicale/__init__.py index d2ca309..92a4ea3 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -278,30 +278,21 @@ class Application(object): else: user = password = None - if not items or function == self.options or \ - auth.is_authenticated(user, password) if user else True: + read_allowed_items, write_allowed_items = \ + self.collect_allowed_items(items, user) - read_allowed_items, write_allowed_items = \ - self.collect_allowed_items(items, user) - - if read_allowed_items or write_allowed_items or \ - function == self.options or not items: - # Collections found, or OPTIONS request, or no items at all - status, headers, answer = function( - environ, read_allowed_items, write_allowed_items, content, - user) - elif not user: - # Unknown or unauthorized user - log.LOGGER.info("%s refused" % (user or "Anonymous user")) - status = client.UNAUTHORIZED - headers = { - "WWW-Authenticate": - "Basic realm=\"%s\"" % config.get("server", "realm")} - answer = None - else: - # Good user but has no rights to any of the given collections - status, headers, answer = NOT_ALLOWED + if ((read_allowed_items or write_allowed_items) + and auth.is_authenticated(user, password)) or \ + function == self.options or not items: + # Collections found, or OPTIONS request, or no items at all + status, headers, answer = function( + environ, read_allowed_items, write_allowed_items, content, + user) else: + status, headers, answer = NOT_ALLOWED + + if (status, headers, answer) == NOT_ALLOWED and \ + not auth.is_authenticated(user, password): # Unknown or unauthorized user log.LOGGER.info("%s refused" % (user or "Anonymous user")) status = client.UNAUTHORIZED diff --git a/radicale/rights.py b/radicale/rights.py index e43c6dd..7f741a1 100644 --- a/radicale/rights.py +++ b/radicale/rights.py @@ -91,9 +91,10 @@ def _read_from_sections(user, collection, permission): def authorized(user, collection, right): - """Check if the user is allowed to read or write the collection.""" + """Check if the user is allowed to read or write the collection. + + If the user is empty it checks for anonymous rights + """ rights_type = config.get("rights", "type").lower() - return rights_type == "none" or ( - (True if not user else user) and _read_from_sections( - user if user else "", collection.url.rstrip("/") or "/", right) - ) + return rights_type == "none" or (_read_from_sections( + user or "", collection.url.rstrip("/") or "/", right))