Remove extra auth, rights and storage modules
This commit is contained in:
parent
1c4acc44a8
commit
1001bcb676
74
config
74
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]
|
||||
|
||||
|
@ -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.
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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)
|
@ -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
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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 = {
|
@ -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()
|
@ -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,50 +16,88 @@
|
||||
# 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
|
||||
|
||||
|
||||
class Collection(filesystem.Collection):
|
||||
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()
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
def save(self, text):
|
||||
self._create_dirs()
|
||||
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
|
||||
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 write(self):
|
||||
self._create_dirs()
|
||||
for component in self.components:
|
||||
text = ical.serialize(
|
||||
self.tag, self.headers, [component] + self.timezones)
|
||||
name = component.name
|
||||
if not pathutils.is_safe_filesystem_path_component(name):
|
||||
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)
|
||||
|
||||
def delete(self):
|
||||
shutil.rmtree(self._filesystem_path)
|
||||
os.remove(self._props_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)
|
@ -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
|
@ -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.
|
||||
|
||||
"""
|
@ -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)
|
Loading…
Reference in New Issue
Block a user