diff --git a/config b/config index 3f59c2a..2d93905 100644 --- a/config +++ b/config @@ -47,9 +47,12 @@ stock = utf-8 [auth] # Authentication method -# Value: None | htpasswd | IMAP | LDAP | PAM | courier | http +# Value: None | htpasswd | IMAP | LDAP | PAM | courier | http | custom type = None +# custom auth handler +custom_handler = + # Htpasswd filename htpasswd_filename = /etc/radicale/users # Htpasswd encryption method @@ -100,19 +103,29 @@ committer = Firstname Lastname [rights] +# Rights backend +# Value: regex | custom +backend = regex + # Rights management method # Value: None | owner_only | owner_write | from_file type = None +# Rights custom handler +custom_handler = + # File for rights management from_file file = ~/.config/radicale/rights [storage] # Storage backend -# Value: filesystem | multifilesystem | database +# Value: filesystem | multifilesystem | database | custom type = filesystem +# Custom storage handler +custom_handler = + # Folder for storing local collections, created if not present filesystem_folder = ~/.config/radicale/collections diff --git a/radicale/__init__.py b/radicale/__init__.py index b383638..9222e9c 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -127,6 +127,7 @@ class Application(object): super(Application, self).__init__() auth.load() storage.load() + rights.load() self.encoding = config.get("encoding", "request") if config.getboolean("logging", "full_environment"): self.headers_log = lambda environ: environ diff --git a/radicale/auth/__init__.py b/radicale/auth/__init__.py index 5e7c01b..5dbdde4 100644 --- a/radicale/auth/__init__.py +++ b/radicale/auth/__init__.py @@ -34,13 +34,17 @@ def load(): 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 + # Override auth.is_authenticated + sys.modules[__name__].is_authenticated = module.is_authenticated + return module def is_authenticated(user, password): diff --git a/radicale/config.py b/radicale/config.py index 71d27a8..eecd42c 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -55,6 +55,7 @@ INITIAL_CONFIG = { "stock": "utf-8"}, "auth": { "type": "None", + "custom_handler": "", "htpasswd_filename": "/etc/radicale/users", "htpasswd_encryption": "crypt", "imap_hostname": "localhost", @@ -75,10 +76,13 @@ INITIAL_CONFIG = { "git": { "committer": "Radicale "}, "rights": { + "backend": "regex", "type": "None", + "custom_handler": "", "file": "~/.config/radicale/rights"}, "storage": { "type": "filesystem", + "custom_handler": "", "filesystem_folder": os.path.expanduser( "~/.config/radicale/collections"), "database_url": ""}, diff --git a/radicale/rights/__init__.py b/radicale/rights/__init__.py new file mode 100644 index 0000000..789953b --- /dev/null +++ b/radicale/rights/__init__.py @@ -0,0 +1,50 @@ +# -*- 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 . + +""" +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", "backend") + if storage_type == 'custom': + rights_module = config.get("rights", "custom_handler") + __import__(rights_module) + module = sys.modules[rights_module] + else: + root_module = __import__( + "rights.%s" % storage_type, globals=globals(), level=2) + module = getattr(root_module, storage_type) + sys.modules[__name__].authorized = module.authorized + return module + + +def authorized(user, collection, right): + """ Check when user has rights on collection + This method is overriden when appropriate rights backend loaded. + """ + raise NotImplementedError() diff --git a/radicale/rights.py b/radicale/rights/regex.py similarity index 99% rename from radicale/rights.py rename to radicale/rights/regex.py index c772c1a..21a828b 100644 --- a/radicale/rights.py +++ b/radicale/rights/regex.py @@ -38,7 +38,7 @@ Leading or ending slashes are trimmed from collection's path. import re import os.path -from . import config, log +from .. import config, log # Manage Python2/3 different modules # pylint: disable=F0401 diff --git a/radicale/storage/__init__.py b/radicale/storage/__init__.py index ca9dad3..ab698f4 100644 --- a/radicale/storage/__init__.py +++ b/radicale/storage/__init__.py @@ -23,15 +23,20 @@ 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") - root_module = __import__( - "storage.%s" % storage_type, globals=globals(), level=2) - module = getattr(root_module, 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 diff --git a/setup.py b/setup.py index afe8e96..7318dd0 100755 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ setup( "Radicale-%s.tar.gz" % radicale.VERSION), license="GNU GPL v3", platforms="Any", - packages=["radicale", "radicale.auth", "radicale.storage"], + packages=["radicale", "radicale.auth", "radicale.storage", "radicale.rights"], provides=["radicale"], scripts=["bin/radicale"], keywords=["calendar", "addressbook", "CalDAV", "CardDAV"], diff --git a/tests/__init__.py b/tests/__init__.py index 0d5678c..6c4dd9b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -21,27 +21,16 @@ Tests for Radicale. """ -import base64 -import hashlib import os -import shutil import sys -import tempfile -from dulwich.repo import Repo from io import BytesIO sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) -import radicale - os.environ["RADICALE_CONFIG"] = os.path.join(os.path.dirname( os.path.dirname(__file__)), "config") -from radicale import config -from radicale.auth import htpasswd from .helpers import get_file_content -from sqlalchemy.orm import sessionmaker -from sqlalchemy import create_engine class BaseTest(object): @@ -73,73 +62,3 @@ class BaseTest(object): """Put the response values into the current application.""" self.application._status = status self.application._headers = headers - - -class FileSystem(BaseTest): - """Base class for filesystem tests.""" - storage_type = "filesystem" - - def setup(self): - """Setup function for each test.""" - self.colpath = tempfile.mkdtemp() - config.set("storage", "type", self.storage_type) - from radicale.storage import filesystem - filesystem.FOLDER = self.colpath - filesystem.GIT_REPOSITORY = None - self.application = radicale.Application() - - def teardown(self): - """Teardown function for each test.""" - shutil.rmtree(self.colpath) - - -class MultiFileSystem(FileSystem): - """Base class for multifilesystem tests.""" - storage_type = "multifilesystem" - - -class DataBaseSystem(BaseTest): - """Base class for database tests""" - def setup(self): - config.set("storage", "type", "database") - config.set("storage", "database_url", "sqlite://") - from radicale.storage import database - database.Session = sessionmaker() - database.Session.configure(bind=create_engine("sqlite://")) - session = database.Session() - for st in get_file_content("schema.sql").split(";"): - session.execute(st) - session.commit() - self.application = radicale.Application() - - -class GitFileSystem(FileSystem): - """Base class for filesystem tests using Git""" - def setup(self): - super(GitFileSystem, self).setup() - Repo.init(self.colpath) - from radicale.storage import filesystem - filesystem.GIT_REPOSITORY = Repo(self.colpath) - - -class GitMultiFileSystem(GitFileSystem, MultiFileSystem): - """Base class for multifilesystem tests using Git""" - - -class HtpasswdAuthSystem(BaseTest): - """Base class to test Radicale with Htpasswd authentication""" - def setup(self): - self.colpath = tempfile.mkdtemp() - htpasswd_file_path = os.path.join(self.colpath, ".htpasswd") - with open(htpasswd_file_path, "wb") as fd: - fd.write(b"tmp:{SHA}" + base64.b64encode( - hashlib.sha1(b"bepo").digest())) - config.set("auth", "type", "htpasswd") - self.userpass = "dG1wOmJlcG8=" - self.application = radicale.Application() - htpasswd.FILENAME = htpasswd_file_path - htpasswd.ENCRYPTION = "sha1" - - def teardown(self): - config.set("auth", "type", "None") - radicale.auth.is_authenticated = lambda *_: True diff --git a/tests/custom/__init__.py b/tests/custom/__init__.py new file mode 100644 index 0000000..bf893c0 --- /dev/null +++ b/tests/custom/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 \ No newline at end of file diff --git a/tests/custom/auth.py b/tests/custom/auth.py new file mode 100644 index 0000000..6fc1352 --- /dev/null +++ b/tests/custom/auth.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2008 Nicolas Kandel +# Copyright © 2008 Pascal Halter +# Copyright © 2008-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 . + +""" +Custom authentication. + +Just check username for testing + +""" + + +def is_authenticated(user, password): + return user == 'tmp' diff --git a/tests/custom/storage.py b/tests/custom/storage.py new file mode 100644 index 0000000..bc12356 --- /dev/null +++ b/tests/custom/storage.py @@ -0,0 +1,30 @@ +# -*- 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 . + +""" +Custom storage backend. + +Copy of filesystem storage backend for testing + +""" + +from radicale.storage import filesystem + + +class Collection(filesystem.Collection): + """Collection stored in a flat ical file.""" diff --git a/tests/test_auth.py b/tests/test_auth.py index a6de517..0a1f61e 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -22,22 +22,52 @@ Radicale tests with simple requests and authentication. """ -from nose import with_setup -from . import HtpasswdAuthSystem +import base64 +import hashlib +import os +import radicale +import tempfile +from radicale import config +from radicale.auth import htpasswd +from tests import BaseTest -class TestBaseAuthRequests(HtpasswdAuthSystem): +class TestBaseAuthRequests(BaseTest): """ Tests basic requests with auth. - ..note Only htpasswd works at the moment since - it requires to spawn processes running servers for - others auth methods (ldap). + We should setup auth for each type before create Application object """ - @with_setup(HtpasswdAuthSystem.setup, HtpasswdAuthSystem.teardown) + def setup(self): + self.userpass = "dG1wOmJlcG8=" + + def teardown(self): + config.set("auth", "type", "None") + radicale.auth.is_authenticated = lambda *_: True + def test_root(self): - """Tests a GET request at "/".""" + self.colpath = tempfile.mkdtemp() + htpasswd_file_path = os.path.join(self.colpath, ".htpasswd") + with open(htpasswd_file_path, "wb") as fd: + fd.write(b"tmp:{SHA}" + base64.b64encode( + hashlib.sha1(b"bepo").digest())) + config.set("auth", "type", "htpasswd") + + htpasswd.FILENAME = htpasswd_file_path + htpasswd.ENCRYPTION = "sha1" + + self.application = radicale.Application() + + status, headers, answer = self.request( + "GET", "/", HTTP_AUTHORIZATION=self.userpass) + assert status == 200 + assert "Radicale works!" in answer + + def test_custom(self): + config.set("auth", "type", "custom") + config.set("auth", "custom_handler", "tests.custom.auth") + self.application = radicale.Application() status, headers, answer = self.request( "GET", "/", HTTP_AUTHORIZATION=self.userpass) assert status == 200 diff --git a/tests/test_base.py b/tests/test_base.py index 071c44d..cc1fc3d 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -21,10 +21,15 @@ Radicale tests with simple requests. """ -from . import (FileSystem, MultiFileSystem, DataBaseSystem, - GitFileSystem, GitMultiFileSystem) from .helpers import get_file_content -import sys +import radicale +import shutil +import tempfile +from dulwich.repo import Repo +from radicale import config +from sqlalchemy.orm import sessionmaker +from sqlalchemy import create_engine +from tests import BaseTest class BaseRequests(object): @@ -81,10 +86,72 @@ class BaseRequests(object): status, headers, answer = self.request("GET", "/calendar.ics/") assert "VEVENT" not in answer -# Generate classes with different configs -cl_list = [FileSystem, MultiFileSystem, DataBaseSystem, - GitFileSystem, GitMultiFileSystem] -for cl in cl_list: - classname = "Test%s" % cl.__name__ - setattr(sys.modules[__name__], - classname, type(classname, (BaseRequests, cl), {})) + +class TestFileSystem(BaseRequests, BaseTest): + """Base class for filesystem tests.""" + storage_type = "filesystem" + + def setup(self): + """Setup function for each test.""" + self.colpath = tempfile.mkdtemp() + config.set("storage", "type", self.storage_type) + from radicale.storage import filesystem + filesystem.FOLDER = self.colpath + filesystem.GIT_REPOSITORY = None + self.application = radicale.Application() + + def teardown(self): + """Teardown function for each test.""" + shutil.rmtree(self.colpath) + + +class TestMultiFileSystem(TestFileSystem): + """Base class for multifilesystem tests.""" + storage_type = "multifilesystem" + + +class TestDataBaseSystem(BaseRequests, BaseTest): + """Base class for database tests""" + def setup(self): + config.set("storage", "type", "database") + config.set("storage", "database_url", "sqlite://") + from radicale.storage import database + database.Session = sessionmaker() + database.Session.configure(bind=create_engine("sqlite://")) + session = database.Session() + for st in get_file_content("schema.sql").split(";"): + session.execute(st) + session.commit() + self.application = radicale.Application() + + +class TestGitFileSystem(TestFileSystem): + """Base class for filesystem tests using Git""" + def setup(self): + super(TestGitFileSystem, self).setup() + Repo.init(self.colpath) + from radicale.storage import filesystem + filesystem.GIT_REPOSITORY = Repo(self.colpath) + + +class TestGitMultiFileSystem(TestGitFileSystem, TestMultiFileSystem): + """Base class for multifilesystem tests using Git""" + + +class TestCustomStorageSystem(BaseRequests, BaseTest): + """Base class for custom backend tests.""" + storage_type = "custom" + + def setup(self): + """Setup function for each test.""" + self.colpath = tempfile.mkdtemp() + config.set("storage", "type", self.storage_type) + config.set("storage", "custom_handler", "tests.custom.storage") + from tests.custom import storage + storage.FOLDER = self.colpath + storage.GIT_REPOSITORY = None + self.application = radicale.Application() + + def teardown(self): + """Teardown function for each test.""" + shutil.rmtree(self.colpath) \ No newline at end of file