Code cleaned and modules renamed
*Radicale is probably broken now*
This commit is contained in:
parent
a17ad1b6a3
commit
45afac5353
10
config
10
config
@ -36,8 +36,8 @@ request = utf-8
|
|||||||
stock = utf-8
|
stock = utf-8
|
||||||
|
|
||||||
|
|
||||||
[acl]
|
[auth]
|
||||||
# Access method
|
# Authentication method
|
||||||
# Value: None | htpasswd | LDAP | PAM | courier
|
# Value: None | htpasswd | LDAP | PAM | courier
|
||||||
type = None
|
type = None
|
||||||
|
|
||||||
@ -78,6 +78,12 @@ pam_group_membership =
|
|||||||
courier_socket =
|
courier_socket =
|
||||||
|
|
||||||
|
|
||||||
|
[rights]
|
||||||
|
# Rights management method
|
||||||
|
# Value: None | owner_only
|
||||||
|
type = None
|
||||||
|
|
||||||
|
|
||||||
[storage]
|
[storage]
|
||||||
# Storage backend
|
# Storage backend
|
||||||
type = filesystem
|
type = filesystem
|
||||||
|
@ -200,7 +200,7 @@ class Application(object):
|
|||||||
|
|
||||||
# Check rights
|
# Check rights
|
||||||
if not items or not access or function == self.options:
|
if not items or not access or function == self.options:
|
||||||
# No collection, or no acl, or OPTIONS request: don't check rights
|
# No collection, or no auth, or OPTIONS request: don't check rights
|
||||||
status, headers, answer = function(environ, items, content, None)
|
status, headers, answer = function(environ, items, content, None)
|
||||||
else:
|
else:
|
||||||
# Ask authentication backend to check rights
|
# Ask authentication backend to check rights
|
||||||
@ -213,23 +213,22 @@ class Application(object):
|
|||||||
else:
|
else:
|
||||||
user = password = None
|
user = password = None
|
||||||
|
|
||||||
|
|
||||||
if access.is_authenticated(user, password):
|
if access.is_authenticated(user, password):
|
||||||
|
|
||||||
last_collection_allowed = None
|
last_collection_allowed = None
|
||||||
allowed_items = []
|
allowed_items = []
|
||||||
for item in items:
|
for item in items:
|
||||||
log.LOGGER.debug("Testing %s" % (item.name))
|
log.LOGGER.debug("Testing %s" % (item.name))
|
||||||
if not isinstance(item, ical.Collection):
|
if not isinstance(item, ical.Collection):
|
||||||
# item is not a colleciton, it's the child of the last
|
# item is not a colleciton, it's the child of the last
|
||||||
# collection we've met in the loop. Only add this item if
|
# collection we've met in the loop. Only add this item
|
||||||
# this last collection was allowed. log.LOGGER.info("not a collection: " + collection.name)
|
# if this last collection was allowed.
|
||||||
# collections.append(collection)
|
|
||||||
if last_collection_allowed:
|
if last_collection_allowed:
|
||||||
allowed_items.append(item)
|
allowed_items.append(item)
|
||||||
else:
|
else:
|
||||||
if access.may_read(user, item) or access.may_write(user, item):
|
if access.read_authorized(user, item) or \
|
||||||
log.LOGGER.info(user + "has access to " + item.name)
|
access.write_authorized(user, item):
|
||||||
|
log.LOGGER.info("%s has access to %s" % (
|
||||||
|
user, item.name))
|
||||||
last_collection_allowed = True
|
last_collection_allowed = True
|
||||||
allowed_items.append(item)
|
allowed_items.append(item)
|
||||||
else:
|
else:
|
||||||
@ -242,18 +241,17 @@ class Application(object):
|
|||||||
else:
|
else:
|
||||||
# Good user and no collections found, redirect user to home
|
# Good user and no collections found, redirect user to home
|
||||||
location = "/%s/" % str(quote(user))
|
location = "/%s/" % str(quote(user))
|
||||||
if path != location:
|
if path == location:
|
||||||
|
# Send answer anyway since else we're getting into a
|
||||||
|
# redirect loop
|
||||||
|
status, headers, answer = function(
|
||||||
|
environ, allowed_items, content, user)
|
||||||
|
else:
|
||||||
log.LOGGER.info("redirecting to %s" % location)
|
log.LOGGER.info("redirecting to %s" % location)
|
||||||
status = client.FOUND
|
status = client.FOUND
|
||||||
headers = {"Location": location}
|
headers = {"Location": location}
|
||||||
answer = "Redirecting to %s" % location
|
answer = "Redirecting to %s" % location
|
||||||
else:
|
else:
|
||||||
# Send answer anyway since else we're getting into a redirect loop
|
|
||||||
status, headers, answer = function(
|
|
||||||
environ, allowed_items, content, user)
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
# Unknown or unauthorized user
|
# Unknown or unauthorized user
|
||||||
log.LOGGER.info(
|
log.LOGGER.info(
|
||||||
"%s refused" % (user or "Anonymous user"))
|
"%s refused" % (user or "Anonymous user"))
|
||||||
@ -263,9 +261,6 @@ class Application(object):
|
|||||||
"Basic realm=\"Radicale Server - Password Required\""}
|
"Basic realm=\"Radicale Server - Password Required\""}
|
||||||
answer = None
|
answer = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Set content length
|
# Set content length
|
||||||
if answer:
|
if answer:
|
||||||
log.LOGGER.debug(
|
log.LOGGER.debug(
|
||||||
@ -280,17 +275,13 @@ class Application(object):
|
|||||||
# Return response content
|
# Return response content
|
||||||
return [answer] if answer else []
|
return [answer] if answer else []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def response_not_allowed(self):
|
def response_not_allowed(self):
|
||||||
|
"""Return a standard "not allowed" response."""
|
||||||
headers = {
|
headers = {
|
||||||
"WWW-Authenticate":
|
"WWW-Authenticate":
|
||||||
"Basic realm=\"Radicale Server - Password Required\""}
|
"Basic realm=\"Radicale Server - Password Required\""}
|
||||||
return client.FORBIDDEN, headers, None
|
return client.FORBIDDEN, headers, None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# All these functions must have the same parameters, some are useless
|
# All these functions must have the same parameters, some are useless
|
||||||
# pylint: disable=W0612,W0613,R0201
|
# pylint: disable=W0612,W0613,R0201
|
||||||
|
|
||||||
@ -311,20 +302,15 @@ class Application(object):
|
|||||||
etag = environ.get("HTTP_IF_MATCH", item.etag).replace("\\", "")
|
etag = environ.get("HTTP_IF_MATCH", item.etag).replace("\\", "")
|
||||||
if etag == item.etag:
|
if etag == item.etag:
|
||||||
# No ETag precondition or precondition verified, delete item
|
# No ETag precondition or precondition verified, delete item
|
||||||
if access.may_write(user, collection):
|
if access.write_authorized(user, collection):
|
||||||
answer = xmlutils.delete(environ["PATH_INFO"], collection)
|
answer = xmlutils.delete(environ["PATH_INFO"], collection)
|
||||||
return client.OK, {}, answer
|
return client.OK, {}, answer
|
||||||
else:
|
else:
|
||||||
return self.response_not_allowed()
|
return self.response_not_allowed()
|
||||||
|
|
||||||
|
|
||||||
# No item or ETag precondition not verified, do not delete item
|
# No item or ETag precondition not verified, do not delete item
|
||||||
return client.PRECONDITION_FAILED, {}, None
|
return client.PRECONDITION_FAILED, {}, None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get(self, environ, collections, content, user):
|
def get(self, environ, collections, content, user):
|
||||||
"""Manage GET request.
|
"""Manage GET request.
|
||||||
|
|
||||||
@ -346,7 +332,7 @@ class Application(object):
|
|||||||
# Get collection item
|
# Get collection item
|
||||||
item = collection.get_item(item_name)
|
item = collection.get_item(item_name)
|
||||||
if item:
|
if item:
|
||||||
if access.may_read(user, collection):
|
if access.read_authorized(user, collection):
|
||||||
items = collection.timezones
|
items = collection.timezones
|
||||||
items.append(item)
|
items.append(item)
|
||||||
answer_text = ical.serialize(
|
answer_text = ical.serialize(
|
||||||
@ -358,11 +344,11 @@ class Application(object):
|
|||||||
return client.GONE, {}, None
|
return client.GONE, {}, None
|
||||||
else:
|
else:
|
||||||
# Create the collection if it does not exist
|
# Create the collection if it does not exist
|
||||||
if not collection.exists and access.may_write(user, collection):
|
if not collection.exists and access.write_authorized(user, collection):
|
||||||
log.LOGGER.debug("creating collection " + collection.name)
|
log.LOGGER.debug("creating collection " + collection.name)
|
||||||
collection.write()
|
collection.write()
|
||||||
|
|
||||||
if access.may_read(user, collection):
|
if access.read_authorized(user, collection):
|
||||||
# Get whole collection
|
# Get whole collection
|
||||||
answer_text = collection.text
|
answer_text = collection.text
|
||||||
etag = collection.etag
|
etag = collection.etag
|
||||||
@ -376,18 +362,11 @@ class Application(object):
|
|||||||
answer = answer_text.encode(self.encoding)
|
answer = answer_text.encode(self.encoding)
|
||||||
return client.OK, headers, answer
|
return client.OK, headers, answer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def head(self, environ, collections, content, user):
|
def head(self, environ, collections, content, user):
|
||||||
"""Manage HEAD request."""
|
"""Manage HEAD request."""
|
||||||
status, headers, answer = self.get(environ, collections, content, user)
|
status, headers, answer = self.get(environ, collections, content, user)
|
||||||
return status, headers, None
|
return status, headers, None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def mkcalendar(self, environ, collections, content, user):
|
def mkcalendar(self, environ, collections, content, user):
|
||||||
"""Manage MKCALENDAR request."""
|
"""Manage MKCALENDAR request."""
|
||||||
collection = collections[0]
|
collection = collections[0]
|
||||||
@ -399,15 +378,12 @@ class Application(object):
|
|||||||
with collection.props as collection_props:
|
with collection.props as collection_props:
|
||||||
for key, value in props.items():
|
for key, value in props.items():
|
||||||
collection_props[key] = value
|
collection_props[key] = value
|
||||||
if access.may_write(user, collection):
|
if access.write_authorized(user, collection):
|
||||||
collection.write()
|
collection.write()
|
||||||
else:
|
else:
|
||||||
return self.response_not_allowed()
|
return self.response_not_allowed()
|
||||||
return client.CREATED, {}, None
|
return client.CREATED, {}, None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def mkcol(self, environ, collections, content, user):
|
def mkcol(self, environ, collections, content, user):
|
||||||
"""Manage MKCOL request."""
|
"""Manage MKCOL request."""
|
||||||
collection = collections[0]
|
collection = collections[0]
|
||||||
@ -415,15 +391,12 @@ class Application(object):
|
|||||||
with collection.props as collection_props:
|
with collection.props as collection_props:
|
||||||
for key, value in props.items():
|
for key, value in props.items():
|
||||||
collection_props[key] = value
|
collection_props[key] = value
|
||||||
if access.may_write(user, collection):
|
if access.write_authorized(user, collection):
|
||||||
collection.write()
|
collection.write()
|
||||||
else:
|
else:
|
||||||
return self.response_not_allowed()
|
return self.response_not_allowed()
|
||||||
return client.CREATED, {}, None
|
return client.CREATED, {}, None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def move(self, environ, collections, content, user):
|
def move(self, environ, collections, content, user):
|
||||||
"""Manage MOVE request."""
|
"""Manage MOVE request."""
|
||||||
from_collection = collections[0]
|
from_collection = collections[0]
|
||||||
@ -439,7 +412,8 @@ class Application(object):
|
|||||||
to_path, to_name = to_url.rstrip("/").rsplit("/", 1)
|
to_path, to_name = to_url.rstrip("/").rsplit("/", 1)
|
||||||
to_collection = ical.Collection.from_path(
|
to_collection = ical.Collection.from_path(
|
||||||
to_path, depth="0")[0]
|
to_path, depth="0")[0]
|
||||||
if access.may_write(user, to_collection) and access.may_write(user.from_collection):
|
if access.write_authorized(user, to_collection) and \
|
||||||
|
access.write_authorized(user.from_collection):
|
||||||
to_collection.append(to_name, item.text)
|
to_collection.append(to_name, item.text)
|
||||||
from_collection.remove(from_name)
|
from_collection.remove(from_name)
|
||||||
return client.CREATED, {}, None
|
return client.CREATED, {}, None
|
||||||
@ -455,22 +429,14 @@ class Application(object):
|
|||||||
# Moving collections, not supported
|
# Moving collections, not supported
|
||||||
return client.FORBIDDEN, {}, None
|
return client.FORBIDDEN, {}, None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def options(self, environ, collections, content, user):
|
def options(self, environ, collections, content, user):
|
||||||
"""Manage OPTIONS request."""
|
"""Manage OPTIONS request."""
|
||||||
headers = {
|
headers = {
|
||||||
"Allow": "DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, " \
|
"Allow": ("DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, "
|
||||||
"OPTIONS, PROPFIND, PROPPATCH, PUT, REPORT",
|
"OPTIONS, PROPFIND, PROPPATCH, PUT, REPORT"),
|
||||||
"DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol"}
|
"DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol"}
|
||||||
return client.OK, headers, None
|
return client.OK, headers, None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def propfind(self, environ, collections, content, user):
|
def propfind(self, environ, collections, content, user):
|
||||||
"""Manage PROPFIND request."""
|
"""Manage PROPFIND request."""
|
||||||
headers = {
|
headers = {
|
||||||
@ -480,10 +446,6 @@ class Application(object):
|
|||||||
environ["PATH_INFO"], content, collections, user)
|
environ["PATH_INFO"], content, collections, user)
|
||||||
return client.MULTI_STATUS, headers, answer
|
return client.MULTI_STATUS, headers, answer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def proppatch(self, environ, collections, content, user):
|
def proppatch(self, environ, collections, content, user):
|
||||||
"""Manage PROPPATCH request."""
|
"""Manage PROPPATCH request."""
|
||||||
collection = collections[0]
|
collection = collections[0]
|
||||||
@ -493,10 +455,6 @@ class Application(object):
|
|||||||
"Content-Type": "text/xml"}
|
"Content-Type": "text/xml"}
|
||||||
return client.MULTI_STATUS, headers, answer
|
return client.MULTI_STATUS, headers, answer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def put(self, environ, collections, content, user):
|
def put(self, environ, collections, content, user):
|
||||||
"""Manage PUT request."""
|
"""Manage PUT request."""
|
||||||
collection = collections[0]
|
collection = collections[0]
|
||||||
@ -513,13 +471,13 @@ class Application(object):
|
|||||||
# Case 1: No item and no ETag precondition: Add new item
|
# Case 1: No item and no ETag precondition: Add new item
|
||||||
# Case 2: Item and ETag precondition verified: Modify item
|
# Case 2: Item and ETag precondition verified: Modify item
|
||||||
# Case 3: Item and no Etag precondition: Force modifying item
|
# Case 3: Item and no Etag precondition: Force modifying item
|
||||||
if access.may_write(user, collection):
|
if access.write_authorized(user, collection):
|
||||||
xmlutils.put(environ["PATH_INFO"], content, collection)
|
xmlutils.put(environ["PATH_INFO"], content, collection)
|
||||||
status = client.CREATED
|
status = client.CREATED
|
||||||
# Try to return the etag in the header
|
# Try to return the etag in the header.
|
||||||
# If the added item does't have the same name as the one given by
|
# If the added item does't have the same name as the one given
|
||||||
# the client, then there's no obvious way to generate an etag, we
|
# by the client, then there's no obvious way to generate an
|
||||||
# can safely ignore it.
|
# etag, we can safely ignore it.
|
||||||
new_item = collection.get_item(item_name)
|
new_item = collection.get_item(item_name)
|
||||||
if new_item:
|
if new_item:
|
||||||
headers["ETag"] = new_item.etag
|
headers["ETag"] = new_item.etag
|
||||||
@ -530,16 +488,11 @@ class Application(object):
|
|||||||
status = client.PRECONDITION_FAILED
|
status = client.PRECONDITION_FAILED
|
||||||
return status, headers, None
|
return status, headers, None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def report(self, environ, collections, content, user):
|
def report(self, environ, collections, content, user):
|
||||||
"""Manage REPORT request."""
|
"""Manage REPORT request."""
|
||||||
collection = collections[0]
|
collection = collections[0]
|
||||||
headers = {"Content-Type": "text/xml"}
|
headers = {"Content-Type": "text/xml"}
|
||||||
if access.may_read(user, collection):
|
if access.read_authorized(user, collection):
|
||||||
answer = xmlutils.report(environ["PATH_INFO"], content, collection)
|
answer = xmlutils.report(environ["PATH_INFO"], content, collection)
|
||||||
return client.MULTI_STATUS, headers, answer
|
return client.MULTI_STATUS, headers, answer
|
||||||
else:
|
else:
|
||||||
|
@ -19,49 +19,43 @@
|
|||||||
"""
|
"""
|
||||||
Radicale access module.
|
Radicale access module.
|
||||||
|
|
||||||
Manages access to collections.
|
Manage access to collections.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
from radicale import auth, rights, log
|
||||||
import sys
|
|
||||||
|
|
||||||
from radicale import acl, authorization, log
|
|
||||||
|
|
||||||
|
AUTH = None
|
||||||
|
RIGHTS = None
|
||||||
|
|
||||||
|
|
||||||
def load():
|
def load():
|
||||||
log.LOGGER.debug("access.load()")
|
"""Load authentication and rights modules."""
|
||||||
global aacl ; aacl = acl.load()
|
global AUTH, RIGHTS
|
||||||
global aauthorization ; aauthorization = authorization.load()
|
AUTH = auth.load()
|
||||||
|
RIGHTS = rights.load()
|
||||||
|
|
||||||
|
|
||||||
def is_authenticated(user, password):
|
def is_authenticated(user, password):
|
||||||
if (not user):
|
"""Check if the user is authenticated."""
|
||||||
# No user given
|
return AUTH.is_authenticated(user, password) if user else False
|
||||||
return False
|
|
||||||
|
|
||||||
return aacl.is_authenticated(user, password)
|
|
||||||
|
|
||||||
|
|
||||||
|
def read_authorized(user, collection):
|
||||||
|
"""Check if the user is allowed to read the collection."""
|
||||||
def may_read(user, collection):
|
if RIGHTS is None:
|
||||||
"""Check if the user is allowed to read the collection"""
|
return True
|
||||||
|
user_authorized = RIGHTS.read_authorized(user, collection)
|
||||||
user_authorized = aauthorization.read_authorized(user, collection)
|
log.LOGGER.debug(
|
||||||
|
"Read %s %s -- %i" % (user, collection.owner, user_authorized))
|
||||||
log.LOGGER.debug("read %s %s -- %i" % (user, collection.owner, user_authorized))
|
|
||||||
return user_authorized
|
return user_authorized
|
||||||
|
|
||||||
|
|
||||||
|
def write_authorized(user, collection):
|
||||||
|
"""Check if the user is allowed to write the collection."""
|
||||||
def may_write(user, collection):
|
if RIGHTS is None:
|
||||||
"""Check if the user is allowed to write the collection"""
|
return True
|
||||||
|
user_authorized = RIGHTS.write_authorized(user, collection)
|
||||||
user_authorized = aauthorization.write_authorized(user, collection)
|
log.LOGGER.debug(
|
||||||
|
"Write %s %s -- %i" % (user, collection.owner, user_authorized))
|
||||||
log.LOGGER.debug("write %s %s -- %i" % (user, collection.owner, user_authorized))
|
|
||||||
return user_authorized
|
return user_authorized
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
IMAP ACL.
|
IMAP authentication.
|
||||||
|
|
||||||
Secure authentication based on the ``imaplib`` module.
|
Secure authentication based on the ``imaplib`` module.
|
||||||
|
|
||||||
@ -32,17 +32,17 @@ Python 3.2 or newer is required for TLS.
|
|||||||
|
|
||||||
import imaplib
|
import imaplib
|
||||||
|
|
||||||
from radicale import acl, config, log
|
from radicale import config, log
|
||||||
|
|
||||||
IMAP_SERVER = config.get("acl", "imap_auth_host_name")
|
IMAP_SERVER = config.get("auth", "imap_auth_host_name")
|
||||||
IMAP_SERVER_PORT = config.get("acl", "imap_auth_host_port")
|
IMAP_SERVER_PORT = config.get("auth", "imap_auth_host_port")
|
||||||
|
|
||||||
|
|
||||||
def is_authenticated(user, password):
|
def is_authenticated(user, password):
|
||||||
"""Check if ``user``/``password`` couple is valid."""
|
"""Check if ``user``/``password`` couple is valid."""
|
||||||
|
|
||||||
log.LOGGER.debug(
|
log.LOGGER.debug(
|
||||||
"[IMAP ACL] Connecting to %s:%s." % (IMAP_SERVER, IMAP_SERVER_PORT,))
|
"[IMAP AUTH] Connecting to %s:%s." % (IMAP_SERVER, IMAP_SERVER_PORT,))
|
||||||
connection = imaplib.IMAP4(host=IMAP_SERVER, port=IMAP_SERVER_PORT)
|
connection = imaplib.IMAP4(host=IMAP_SERVER, port=IMAP_SERVER_PORT)
|
||||||
|
|
||||||
server_is_local = (IMAP_SERVER == "localhost")
|
server_is_local = (IMAP_SERVER == "localhost")
|
||||||
@ -50,20 +50,20 @@ def is_authenticated(user, password):
|
|||||||
connection_is_secure = False
|
connection_is_secure = False
|
||||||
try:
|
try:
|
||||||
connection.starttls()
|
connection.starttls()
|
||||||
log.LOGGER.debug("[IMAP ACL] Server connection changed to TLS.")
|
log.LOGGER.debug("IMAP server connection changed to TLS.")
|
||||||
connection_is_secure = True
|
connection_is_secure = True
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
if not server_is_local:
|
if not server_is_local:
|
||||||
log.LOGGER.error(
|
log.LOGGER.error(
|
||||||
"[IMAP ACL] Python 3.2 or newer is required for TLS.")
|
"Python 3.2 or newer is required for IMAP + TLS.")
|
||||||
except (imaplib.IMAP4.error, imaplib.IMAP4.abort) as exception:
|
except (imaplib.IMAP4.error, imaplib.IMAP4.abort) as exception:
|
||||||
log.LOGGER.warning(
|
log.LOGGER.warning(
|
||||||
"[IMAP ACL] Server at %s failed to accept TLS connection "
|
"IMAP server at %s failed to accept TLS connection "
|
||||||
"because of: %s" % (IMAP_SERVER, exception))
|
"because of: %s" % (IMAP_SERVER, exception))
|
||||||
|
|
||||||
if server_is_local and not connection_is_secure:
|
if server_is_local and not connection_is_secure:
|
||||||
log.LOGGER.warning(
|
log.LOGGER.warning(
|
||||||
"[IMAP ACL] Server is local. "
|
"IMAP server is local. "
|
||||||
"Will allow transmitting unencrypted credentials.")
|
"Will allow transmitting unencrypted credentials.")
|
||||||
|
|
||||||
if connection_is_secure or server_is_local:
|
if connection_is_secure or server_is_local:
|
||||||
@ -71,16 +71,16 @@ def is_authenticated(user, password):
|
|||||||
connection.login(user, password)
|
connection.login(user, password)
|
||||||
connection.logout()
|
connection.logout()
|
||||||
log.LOGGER.debug(
|
log.LOGGER.debug(
|
||||||
"[IMAP ACL] Authenticated user %s "
|
"Authenticated IMAP user %s "
|
||||||
"via %s." % (user, IMAP_SERVER))
|
"via %s." % (user, IMAP_SERVER))
|
||||||
return True
|
return True
|
||||||
except (imaplib.IMAP4.error, imaplib.IMAP4.abort) as exception:
|
except (imaplib.IMAP4.error, imaplib.IMAP4.abort) as exception:
|
||||||
log.LOGGER.error(
|
log.LOGGER.error(
|
||||||
"[IMAP ACL] Server could not authenticate user %s "
|
"IMAP server could not authenticate user %s "
|
||||||
"because of: %s" % (user, exception))
|
"because of: %s" % (user, exception))
|
||||||
else:
|
else:
|
||||||
log.LOGGER.critical(
|
log.LOGGER.critical(
|
||||||
"[IMAP ACL] Server did not support TLS and is not ``localhost``. "
|
"IMAP server did not support TLS and is not ``localhost``. "
|
||||||
"Refusing to transmit passwords under these conditions. "
|
"Refusing to transmit passwords under these conditions. "
|
||||||
"Authentication attempt aborted.")
|
"Authentication attempt aborted.")
|
||||||
return False # authentication failed
|
return False # authentication failed
|
@ -18,7 +18,7 @@
|
|||||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
LDAP ACL.
|
LDAP authentication.
|
||||||
|
|
||||||
Authentication based on the ``python-ldap`` module
|
Authentication based on the ``python-ldap`` module
|
||||||
(http://www.python-ldap.org/).
|
(http://www.python-ldap.org/).
|
||||||
@ -26,16 +26,16 @@ Authentication based on the ``python-ldap`` module
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import ldap
|
import ldap
|
||||||
from radicale import acl, config, log
|
from radicale import config, log
|
||||||
|
|
||||||
|
|
||||||
BASE = config.get("acl", "ldap_base")
|
BASE = config.get("auth", "ldap_base")
|
||||||
ATTRIBUTE = config.get("acl", "ldap_attribute")
|
ATTRIBUTE = config.get("auth", "ldap_attribute")
|
||||||
FILTER = config.get("acl", "ldap_filter")
|
FILTER = config.get("auth", "ldap_filter")
|
||||||
CONNEXION = ldap.initialize(config.get("acl", "ldap_url"))
|
CONNEXION = ldap.initialize(config.get("auth", "ldap_url"))
|
||||||
BINDDN = config.get("acl", "ldap_binddn")
|
BINDDN = config.get("auth", "ldap_binddn")
|
||||||
PASSWORD = config.get("acl", "ldap_password")
|
PASSWORD = config.get("auth", "ldap_password")
|
||||||
SCOPE = getattr(ldap, "SCOPE_%s" % config.get("acl", "ldap_scope").upper())
|
SCOPE = getattr(ldap, "SCOPE_%s" % config.get("auth", "ldap_scope").upper())
|
||||||
|
|
||||||
|
|
||||||
def is_authenticated(user, password):
|
def is_authenticated(user, password):
|
||||||
@ -46,7 +46,7 @@ def is_authenticated(user, password):
|
|||||||
CONNEXION.whoami_s()
|
CONNEXION.whoami_s()
|
||||||
except:
|
except:
|
||||||
log.LOGGER.debug("Reconnecting the LDAP server")
|
log.LOGGER.debug("Reconnecting the LDAP server")
|
||||||
CONNEXION = ldap.initialize(config.get("acl", "ldap_url"))
|
CONNEXION = ldap.initialize(config.get("auth", "ldap_url"))
|
||||||
|
|
||||||
if BINDDN and PASSWORD:
|
if BINDDN and PASSWORD:
|
||||||
log.LOGGER.debug("Initial LDAP bind as %s" % BINDDN)
|
log.LOGGER.debug("Initial LDAP bind as %s" % BINDDN)
|
@ -17,7 +17,7 @@
|
|||||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
PAM ACL.
|
PAM authentication.
|
||||||
|
|
||||||
Authentication based on the ``pam-python`` module.
|
Authentication based on the ``pam-python`` module.
|
||||||
|
|
||||||
@ -27,10 +27,10 @@ import grp
|
|||||||
import pam
|
import pam
|
||||||
import pwd
|
import pwd
|
||||||
|
|
||||||
from radicale import acl, config, log
|
from radicale import config, log
|
||||||
|
|
||||||
|
|
||||||
GROUP_MEMBERSHIP = config.get("acl", "pam_group_membership")
|
GROUP_MEMBERSHIP = config.get("auth", "pam_group_membership")
|
||||||
|
|
||||||
|
|
||||||
def is_authenticated(user, password):
|
def is_authenticated(user, password):
|
@ -1,7 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# This file is part of Radicale Server - Calendar Server
|
# This file is part of Radicale Server - Calendar Server
|
||||||
# Copyright © 2011-2012 Guillaume Ayoub
|
# Copyright © 2008-2012 Guillaume Ayoub
|
||||||
|
# Copyright © 2008 Nicolas Kandel
|
||||||
|
# Copyright © 2008 Pascal Halter
|
||||||
#
|
#
|
||||||
# This library is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -17,31 +19,20 @@
|
|||||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Radicale authorization module.
|
Authentication management.
|
||||||
|
|
||||||
Manages who is authorized to access a collection.
|
|
||||||
|
|
||||||
The policy here is that all authenticated users
|
|
||||||
have read and write access to all collections.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
from radicale import config, log
|
||||||
import sys
|
|
||||||
|
|
||||||
from radicale import authorization, config, log
|
|
||||||
|
|
||||||
|
|
||||||
|
def load():
|
||||||
|
"""Load list of available authentication managers."""
|
||||||
def read_authorized(user, collection):
|
auth_type = config.get("auth", "type")
|
||||||
"""Check if the user is allowed to read the collection"""
|
log.LOGGER.debug("Authentication type is %s" % auth_type)
|
||||||
log.LOGGER.debug("read_authorized '" + user + "' in '" + collection.name + "'");
|
if auth_type == "None":
|
||||||
return True
|
return None
|
||||||
|
else:
|
||||||
|
module = __import__(
|
||||||
def write_authorized(user, collection):
|
"auth.%s" % auth_type, globals=globals(), level=2)
|
||||||
"""Check if the user is allowed to write the collection"""
|
return getattr(module, auth_type)
|
||||||
log.LOGGER.debug("write_authorized '" + user + "' in '" + collection.name + "'");
|
|
||||||
return True
|
|
||||||
|
|
@ -17,16 +17,17 @@
|
|||||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Courier-Authdaemon ACL.
|
Courier-Authdaemon authentication.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import socket
|
import socket
|
||||||
from radicale import acl, config, log
|
|
||||||
|
from radicale import config, log
|
||||||
|
|
||||||
|
|
||||||
COURIER_SOCKET = config.get("acl", "courier_socket")
|
COURIER_SOCKET = config.get("auth", "courier_socket")
|
||||||
|
|
||||||
|
|
||||||
def is_authenticated(user, password):
|
def is_authenticated(user, password):
|
@ -19,7 +19,7 @@
|
|||||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Htpasswd ACL.
|
Htpasswd authentication.
|
||||||
|
|
||||||
Load the list of login/password couples according a the configuration file
|
Load the list of login/password couples according a the configuration file
|
||||||
created by Apache ``htpasswd`` command. Plain-text, crypt and sha1 are
|
created by Apache ``htpasswd`` command. Plain-text, crypt and sha1 are
|
||||||
@ -30,11 +30,11 @@ supported, but md5 is not (see ``htpasswd`` man page to understand why).
|
|||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from radicale import acl, config
|
from radicale import config
|
||||||
|
|
||||||
|
|
||||||
FILENAME = config.get("acl", "htpasswd_filename")
|
FILENAME = config.get("auth", "htpasswd_filename")
|
||||||
ENCRYPTION = config.get("acl", "htpasswd_encryption")
|
ENCRYPTION = config.get("auth", "htpasswd_encryption")
|
||||||
|
|
||||||
|
|
||||||
def _plain(hash_value, password):
|
def _plain(hash_value, password):
|
@ -1,76 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# This file is part of Radicale Server - Calendar Server
|
|
||||||
# Copyright © 2008-2012 Guillaume Ayoub
|
|
||||||
# Copyright © 2008 Nicolas Kandel
|
|
||||||
# Copyright © 2008 Pascal Halter
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Users and rights management.
|
|
||||||
|
|
||||||
This module loads a list of users with access rights, according to the acl
|
|
||||||
configuration.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from radicale import config, log
|
|
||||||
|
|
||||||
|
|
||||||
AUTHORIZATION_PREFIX = "authorization"
|
|
||||||
|
|
||||||
PUBLIC_USERS = []
|
|
||||||
PRIVATE_USERS = []
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _config_users(name):
|
|
||||||
"""Get an iterable of strings from the configuraton string [acl] ``name``.
|
|
||||||
|
|
||||||
The values must be separated by a comma. The whitespace characters are
|
|
||||||
stripped at the beginning and at the end of the values.
|
|
||||||
|
|
||||||
"""
|
|
||||||
for user in config.get(AUTHORIZATION_PREFIX, name).split(","):
|
|
||||||
user = user.strip()
|
|
||||||
yield None if user == "None" else user
|
|
||||||
|
|
||||||
|
|
||||||
def load():
|
|
||||||
"""Load list of available ACL managers."""
|
|
||||||
|
|
||||||
PUBLIC_USERS.extend(_config_users("public_users"))
|
|
||||||
PRIVATE_USERS.extend(_config_users("private_users"))
|
|
||||||
|
|
||||||
authorization_type = config.get(AUTHORIZATION_PREFIX, "type")
|
|
||||||
log.LOGGER.debug("auth type = " + authorization_type)
|
|
||||||
if authorization_type == "None":
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
module = __import__("authorization.%s" % authorization_type, globals=globals(), level=2)
|
|
||||||
return getattr(module, authorization_type)
|
|
||||||
|
|
||||||
|
|
||||||
def may_read(user, collection):
|
|
||||||
if (collection.owner not in PRIVATE_USERS and user != collection.owner):
|
|
||||||
# owner is not private and is not user, forbidden
|
|
||||||
return False
|
|
||||||
|
|
||||||
return read_authorized(user, collection)
|
|
||||||
|
|
||||||
def may_write(user, collection):
|
|
||||||
return write_authorized(user, collection)
|
|
||||||
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# This file is part of Radicale Server - Calendar Server
|
|
||||||
# Copyright © 2011-2012 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/>.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Radicale authorization module.
|
|
||||||
|
|
||||||
Manages who is authorized to access a collection.
|
|
||||||
|
|
||||||
The policy is that the owner may read and write in
|
|
||||||
all collections and some special rights are hardcoded.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from radicale import authorization, config, log
|
|
||||||
from radicale.authorization import owneronly
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def read_authorized(user, collection):
|
|
||||||
"""Check if the user is allowed to read the collection"""
|
|
||||||
log.LOGGER.debug("read_authorized '" + user + "' in '" + collection.owner + "/" + collection.name + "'");
|
|
||||||
|
|
||||||
if owneronly.read_authorized(user, collection):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if user == "user1" and collection.owner == "user2" and collection.name == "user2sharedwithuser1":
|
|
||||||
return True
|
|
||||||
if user == "user2" and collection.owner == "user1" and collection.name == "user1sharedwithuser2":
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def write_authorized(user, collection):
|
|
||||||
"""Check if the user is allowed to write the collection"""
|
|
||||||
log.LOGGER.debug("write_authorized '" + user + "' in '" + collection.owner + "/" + collection.name + "'");
|
|
||||||
|
|
||||||
if owneronly.write_authorized(user, collection):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if user == "user1" and collection.owner == "user2" and collection.name == "user2sharedwithuser1":
|
|
||||||
return True
|
|
||||||
if user == "user2" and collection.owner == "user1" and collection.name == "user1sharedwithuser2":
|
|
||||||
return False
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
@ -49,7 +49,7 @@ INITIAL_CONFIG = {
|
|||||||
"encoding": {
|
"encoding": {
|
||||||
"request": "utf-8",
|
"request": "utf-8",
|
||||||
"stock": "utf-8"},
|
"stock": "utf-8"},
|
||||||
"acl": {
|
"auth": {
|
||||||
"type": "None",
|
"type": "None",
|
||||||
"public_users": "public",
|
"public_users": "public",
|
||||||
"private_users": "private",
|
"private_users": "private",
|
||||||
|
@ -19,39 +19,20 @@
|
|||||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Users management.
|
Rights management.
|
||||||
|
|
||||||
ACL is basically the wrong name here since this package deals with authenticating users.
|
|
||||||
|
|
||||||
The authorization part is done in the package "authorization".
|
|
||||||
|
|
||||||
This module loads a list of users with access rights, according to the acl
|
|
||||||
configuration.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from radicale import config, log
|
from radicale import config, log
|
||||||
|
|
||||||
CONFIG_PREFIX = "acl"
|
|
||||||
|
|
||||||
def _config_users(name):
|
|
||||||
"""Get an iterable of strings from the configuraton string [acl] ``name``.
|
|
||||||
|
|
||||||
The values must be separated by a comma. The whitespace characters are
|
|
||||||
stripped at the beginning and at the end of the values.
|
|
||||||
|
|
||||||
"""
|
|
||||||
for user in config.get(CONFIG_PREFIX, name).split(","):
|
|
||||||
user = user.strip()
|
|
||||||
yield None if user == "None" else user
|
|
||||||
|
|
||||||
|
|
||||||
def load():
|
def load():
|
||||||
"""Load list of available ACL managers."""
|
"""Load list of available ACL managers."""
|
||||||
acl_type = config.get(CONFIG_PREFIX, "type")
|
rights_type = config.get("rights", "type")
|
||||||
log.LOGGER.debug("acl_type = " + acl_type)
|
log.LOGGER.debug("Rights type is %s" % rights_type)
|
||||||
if acl_type == "None":
|
if rights_type == "None":
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
module = __import__("acl.%s" % acl_type, globals=globals(), level=2)
|
module = __import__(
|
||||||
return getattr(module, acl_type)
|
"rights.%s" % rights_type, globals=globals(), level=2)
|
||||||
|
return getattr(module, rights_type)
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# This file is part of Radicale Server - Calendar Server
|
# This file is part of Radicale Server - Calendar Server
|
||||||
# Copyright © 2011-2012 Guillaume Ayoub
|
# Copyright © 2012 Guillaume Ayoub
|
||||||
#
|
#
|
||||||
# This library is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -17,33 +17,18 @@
|
|||||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Radicale authorization module.
|
Owner-only based rights.
|
||||||
|
|
||||||
Manages who is authorized to access a collection.
|
Only owners have read and write access to their own collections.
|
||||||
|
|
||||||
The policy here is that owners have read and write access
|
|
||||||
to their own collections.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from radicale import authorization, config, log
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def read_authorized(user, collection):
|
def read_authorized(user, collection):
|
||||||
"""Check if the user is allowed to read the collection"""
|
"""Check if the user is allowed to read the collection."""
|
||||||
log.LOGGER.debug("read_authorized '" + user + "' in '" + collection.owner + "/" + collection.name + "'");
|
|
||||||
|
|
||||||
return user == collection.owner
|
return user == collection.owner
|
||||||
|
|
||||||
|
|
||||||
def write_authorized(user, collection):
|
def write_authorized(user, collection):
|
||||||
"""Check if the user is allowed to write the collection"""
|
"""Check if the user is allowed to write the collection."""
|
||||||
log.LOGGER.debug("write_authorized '" + user + "' in '" + collection.owner + "/" + collection.name + "'");
|
|
||||||
|
|
||||||
return user == collection.owner
|
return user == collection.owner
|
||||||
|
|
@ -200,7 +200,7 @@ def propfind(path, xml_request, collections, user=None):
|
|||||||
multistatus = ET.Element(_tag("D", "multistatus"))
|
multistatus = ET.Element(_tag("D", "multistatus"))
|
||||||
|
|
||||||
for collection in collections:
|
for collection in collections:
|
||||||
if access.may_read(user, collection):
|
if access.read_authorized(user, collection):
|
||||||
response = _propfind_response(path, collection, props, user)
|
response = _propfind_response(path, collection, props, user)
|
||||||
multistatus.append(response)
|
multistatus.append(response)
|
||||||
|
|
||||||
|
7
setup.py
7
setup.py
@ -51,11 +51,12 @@ setup(
|
|||||||
author="Guillaume Ayoub",
|
author="Guillaume Ayoub",
|
||||||
author_email="guillaume.ayoub@kozea.fr",
|
author_email="guillaume.ayoub@kozea.fr",
|
||||||
url="http://www.radicale.org/",
|
url="http://www.radicale.org/",
|
||||||
download_url="http://pypi.python.org/packages/source/R/Radicale/" \
|
download_url=("http://pypi.python.org/packages/source/R/Radicale/"
|
||||||
"Radicale-%s.tar.gz" % radicale.VERSION,
|
"Radicale-%s.tar.gz" % radicale.VERSION),
|
||||||
license="GNU GPL v3",
|
license="GNU GPL v3",
|
||||||
platforms="Any",
|
platforms="Any",
|
||||||
packages=["radicale", "radicale.acl", "radicale.storage"],
|
packages=[
|
||||||
|
"radicale", "radicale.auth", "radicale.rights", "radicale.storage"],
|
||||||
provides=["radicale"],
|
provides=["radicale"],
|
||||||
scripts=["bin/radicale"],
|
scripts=["bin/radicale"],
|
||||||
keywords=["calendar", "addressbook", "CalDAV", "CardDAV"],
|
keywords=["calendar", "addressbook", "CalDAV", "CardDAV"],
|
||||||
|
Loading…
Reference in New Issue
Block a user