Separation of authentication and authorization. Separation of read and write authorization.
Static test strategies for authentication. Barely tested. Use at your own risk!
This commit is contained in:
parent
83baebd750
commit
e40e68b528
@ -46,7 +46,7 @@ except ImportError:
|
|||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
# pylint: enable=F0401,E0611
|
# pylint: enable=F0401,E0611
|
||||||
|
|
||||||
from radicale import acl, config, ical, log, storage, xmlutils
|
from radicale import config, ical, log, storage, xmlutils, access
|
||||||
|
|
||||||
|
|
||||||
VERSION = "git"
|
VERSION = "git"
|
||||||
@ -119,7 +119,7 @@ class Application(object):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize application."""
|
"""Initialize application."""
|
||||||
super(Application, self).__init__()
|
super(Application, self).__init__()
|
||||||
self.acl = acl.load()
|
access.load()
|
||||||
storage.load()
|
storage.load()
|
||||||
self.encoding = config.get("encoding", "request")
|
self.encoding = config.get("encoding", "request")
|
||||||
if config.getboolean("logging", "full_environment"):
|
if config.getboolean("logging", "full_environment"):
|
||||||
@ -189,15 +189,17 @@ class Application(object):
|
|||||||
else:
|
else:
|
||||||
content = None
|
content = None
|
||||||
|
|
||||||
|
path = environ["PATH_INFO"]
|
||||||
|
|
||||||
# Find collection(s)
|
# Find collection(s)
|
||||||
items = ical.Collection.from_path(
|
items = ical.Collection.from_path(
|
||||||
environ["PATH_INFO"], environ.get("HTTP_DEPTH", "0"))
|
path, environ.get("HTTP_DEPTH", "0"))
|
||||||
|
|
||||||
# Get function corresponding to method
|
# Get function corresponding to method
|
||||||
function = getattr(self, environ["REQUEST_METHOD"].lower())
|
function = getattr(self, environ["REQUEST_METHOD"].lower())
|
||||||
|
|
||||||
# Check rights
|
# Check rights
|
||||||
if not items or not self.acl 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 acl, 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:
|
||||||
@ -211,51 +213,50 @@ class Application(object):
|
|||||||
else:
|
else:
|
||||||
user = password = None
|
user = password = None
|
||||||
|
|
||||||
last_allowed = None
|
|
||||||
|
if access.is_authenticated(user, password):
|
||||||
|
|
||||||
collections = []
|
collections = []
|
||||||
for collection in items:
|
for collection in items:
|
||||||
|
log.LOGGER.debug("Testing %s" % (collection.name))
|
||||||
if not isinstance(collection, ical.Collection):
|
if not isinstance(collection, ical.Collection):
|
||||||
if last_allowed:
|
log.LOGGER.info("not a collection: " + collection.name)
|
||||||
|
# collections.append(collection)
|
||||||
|
elif access.may_read(user, collection) or access.may_write(user, collection):
|
||||||
|
log.LOGGER.info("Has access to " + collection.name)
|
||||||
collections.append(collection)
|
collections.append(collection)
|
||||||
continue
|
|
||||||
|
|
||||||
if collection.owner in acl.PUBLIC_USERS:
|
|
||||||
log.LOGGER.info("Public collection")
|
|
||||||
collections.append(collection)
|
|
||||||
last_allowed = True
|
|
||||||
else:
|
|
||||||
log.LOGGER.info(
|
|
||||||
"Checking rights for collection owned by %s" % (
|
|
||||||
collection.owner or "nobody"))
|
|
||||||
if self.acl.has_right(collection.owner, user, password):
|
|
||||||
log.LOGGER.info(
|
|
||||||
"%s allowed" % (user or "Anonymous user"))
|
|
||||||
collections.append(collection)
|
|
||||||
last_allowed = True
|
|
||||||
else:
|
|
||||||
log.LOGGER.info(
|
|
||||||
"%s refused" % (user or "Anonymous user"))
|
|
||||||
last_allowed = False
|
|
||||||
|
|
||||||
if collections:
|
if collections:
|
||||||
# Collections found
|
# Collections found
|
||||||
status, headers, answer = function(
|
status, headers, answer = function(
|
||||||
environ, collections, content, user)
|
environ, collections, content, user)
|
||||||
elif user and last_allowed is None:
|
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:
|
||||||
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, collections, content, user)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
# Unknown or unauthorized user
|
# Unknown or unauthorized user
|
||||||
|
log.LOGGER.info(
|
||||||
|
"%s refused" % (user or "Anonymous user"))
|
||||||
status = client.UNAUTHORIZED
|
status = client.UNAUTHORIZED
|
||||||
headers = {
|
headers = {
|
||||||
"WWW-Authenticate":
|
"WWW-Authenticate":
|
||||||
"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(
|
||||||
@ -270,6 +271,17 @@ class Application(object):
|
|||||||
# Return response content
|
# Return response content
|
||||||
return [answer] if answer else []
|
return [answer] if answer else []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def response_not_allowed(self):
|
||||||
|
headers = {
|
||||||
|
"WWW-Authenticate":
|
||||||
|
"Basic realm=\"Radicale Server - Password Required\""}
|
||||||
|
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
|
||||||
|
|
||||||
@ -290,12 +302,20 @@ 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):
|
||||||
answer = xmlutils.delete(environ["PATH_INFO"], collection)
|
answer = xmlutils.delete(environ["PATH_INFO"], collection)
|
||||||
return client.OK, {}, answer
|
return client.OK, {}, answer
|
||||||
|
else:
|
||||||
|
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.
|
||||||
|
|
||||||
@ -317,21 +337,28 @@ 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):
|
||||||
items = collection.timezones
|
items = collection.timezones
|
||||||
items.append(item)
|
items.append(item)
|
||||||
answer_text = ical.serialize(
|
answer_text = ical.serialize(
|
||||||
collection.tag, collection.headers, items)
|
collection.tag, collection.headers, items)
|
||||||
etag = item.etag
|
etag = item.etag
|
||||||
|
else:
|
||||||
|
return self.response_not_allowed()
|
||||||
else:
|
else:
|
||||||
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:
|
if not collection.exists and access.may_write(user, collection):
|
||||||
|
log.LOGGER.debug("creating collection " + collection.name)
|
||||||
collection.write()
|
collection.write()
|
||||||
|
|
||||||
|
if access.may_read(user, collection):
|
||||||
# Get whole collection
|
# Get whole collection
|
||||||
answer_text = collection.text
|
answer_text = collection.text
|
||||||
etag = collection.etag
|
etag = collection.etag
|
||||||
|
else:
|
||||||
|
return self.response_not_allowed()
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": collection.mimetype,
|
"Content-Type": collection.mimetype,
|
||||||
@ -340,11 +367,18 @@ 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]
|
||||||
@ -356,9 +390,15 @@ 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):
|
||||||
collection.write()
|
collection.write()
|
||||||
|
else:
|
||||||
|
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]
|
||||||
@ -366,9 +406,15 @@ 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):
|
||||||
collection.write()
|
collection.write()
|
||||||
|
else:
|
||||||
|
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]
|
||||||
@ -384,9 +430,12 @@ 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):
|
||||||
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
|
||||||
|
else:
|
||||||
|
return self.response_not_allowed()
|
||||||
else:
|
else:
|
||||||
# Remote destination server, not supported
|
# Remote destination server, not supported
|
||||||
return client.BAD_GATEWAY, {}, None
|
return client.BAD_GATEWAY, {}, None
|
||||||
@ -397,6 +446,10 @@ 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 = {
|
||||||
@ -405,6 +458,10 @@ class Application(object):
|
|||||||
"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 = {
|
||||||
@ -414,6 +471,10 @@ 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]
|
||||||
@ -423,6 +484,10 @@ 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]
|
||||||
@ -439,8 +504,9 @@ 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):
|
||||||
xmlutils.put(environ["PATH_INFO"], content, collection)
|
xmlutils.put(environ["PATH_INFO"], content, collection)
|
||||||
status = client.NO_CONTENT if item else 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 by
|
||||||
# the client, then there's no obvious way to generate an etag, we
|
# the client, then there's no obvious way to generate an etag, we
|
||||||
@ -448,16 +514,26 @@ class Application(object):
|
|||||||
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
|
||||||
|
else:
|
||||||
|
return self.response_not_allowed()
|
||||||
else:
|
else:
|
||||||
# PUT rejected in all other cases
|
# PUT rejected in all other cases
|
||||||
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):
|
||||||
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:
|
||||||
|
return self.response_not_allowed()
|
||||||
|
|
||||||
# pylint: enable=W0612,W0613,R0201
|
# pylint: enable=W0612,W0613,R0201
|
||||||
|
67
radicale/access.py
Normal file
67
radicale/access.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# -*- 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 access module.
|
||||||
|
|
||||||
|
Manages access to collections.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from radicale import acl, authorization, log
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def load():
|
||||||
|
log.LOGGER.debug("access.load()")
|
||||||
|
global aacl ; aacl = acl.load()
|
||||||
|
global aauthorization ; aauthorization = authorization.load()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def is_authenticated(user, password):
|
||||||
|
if (not user):
|
||||||
|
# No user given
|
||||||
|
return False
|
||||||
|
|
||||||
|
return aacl.is_authenticated(user, password)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def may_read(user, collection):
|
||||||
|
"""Check if the user is allowed to read the collection"""
|
||||||
|
|
||||||
|
user_authorized = aauthorization.read_authorized(user, collection)
|
||||||
|
|
||||||
|
log.LOGGER.debug("read %s %s -- %i" % (user, collection.owner, user_authorized))
|
||||||
|
return user_authorized
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def may_write(user, collection):
|
||||||
|
"""Check if the user is allowed to write the collection"""
|
||||||
|
|
||||||
|
user_authorized = aauthorization.write_authorized(user, collection)
|
||||||
|
|
||||||
|
log.LOGGER.debug("write %s %s -- %i" % (user, collection.owner, user_authorized))
|
||||||
|
return user_authorized
|
@ -38,11 +38,8 @@ IMAP_SERVER = config.get("acl", "imap_auth_host_name")
|
|||||||
IMAP_SERVER_PORT = config.get("acl", "imap_auth_host_port")
|
IMAP_SERVER_PORT = config.get("acl", "imap_auth_host_port")
|
||||||
|
|
||||||
|
|
||||||
def has_right(owner, user, password):
|
def is_authenticated(user, password):
|
||||||
"""Check if ``user``/``password`` couple is valid."""
|
"""Check if ``user``/``password`` couple is valid."""
|
||||||
if not user or (owner not in acl.PRIVATE_USERS and user != owner):
|
|
||||||
# No user given, or owner is not private and is not user, forbidden
|
|
||||||
return False
|
|
||||||
|
|
||||||
log.LOGGER.debug(
|
log.LOGGER.debug(
|
||||||
"[IMAP ACL] Connecting to %s:%s." % (IMAP_SERVER, IMAP_SERVER_PORT,))
|
"[IMAP ACL] Connecting to %s:%s." % (IMAP_SERVER, IMAP_SERVER_PORT,))
|
||||||
|
@ -38,14 +38,10 @@ PASSWORD = config.get("acl", "ldap_password")
|
|||||||
SCOPE = getattr(ldap, "SCOPE_%s" % config.get("acl", "ldap_scope").upper())
|
SCOPE = getattr(ldap, "SCOPE_%s" % config.get("acl", "ldap_scope").upper())
|
||||||
|
|
||||||
|
|
||||||
def has_right(owner, user, password):
|
def is_authenticated(user, password):
|
||||||
"""Check if ``user``/``password`` couple is valid."""
|
"""Check if ``user``/``password`` couple is valid."""
|
||||||
global CONNEXION
|
global CONNEXION
|
||||||
|
|
||||||
if not user or (owner not in acl.PRIVATE_USERS and user != owner):
|
|
||||||
# No user given, or owner is not private and is not user, forbidden
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
CONNEXION.whoami_s()
|
CONNEXION.whoami_s()
|
||||||
except:
|
except:
|
||||||
|
@ -33,11 +33,8 @@ from radicale import acl, config, log
|
|||||||
GROUP_MEMBERSHIP = config.get("acl", "pam_group_membership")
|
GROUP_MEMBERSHIP = config.get("acl", "pam_group_membership")
|
||||||
|
|
||||||
|
|
||||||
def has_right(owner, user, password):
|
def is_authenticated(user, password):
|
||||||
"""Check if ``user``/``password`` couple is valid."""
|
"""Check if ``user``/``password`` couple is valid."""
|
||||||
if not user or (owner not in acl.PRIVATE_USERS and user != owner):
|
|
||||||
# No user given, or owner is not private and is not user, forbidden
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check whether the user exists in the PAM system
|
# Check whether the user exists in the PAM system
|
||||||
try:
|
try:
|
||||||
@ -50,7 +47,7 @@ def has_right(owner, user, password):
|
|||||||
|
|
||||||
# Check whether the group exists
|
# Check whether the group exists
|
||||||
try:
|
try:
|
||||||
members = grp.getgrnam(GROUP_MEMBERSHIP).gr_mem
|
members = grp.getgrnam(GROUP_MEMBERSHIP)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
log.LOGGER.debug(
|
log.LOGGER.debug(
|
||||||
"The PAM membership required group (%s) doesn't exist" %
|
"The PAM membership required group (%s) doesn't exist" %
|
||||||
|
@ -19,19 +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 and rights management.
|
Users 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
|
This module loads a list of users with access rights, according to the acl
|
||||||
configuration.
|
configuration.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from radicale import config
|
from radicale import config, log
|
||||||
|
|
||||||
|
|
||||||
PUBLIC_USERS = []
|
|
||||||
PRIVATE_USERS = []
|
|
||||||
|
|
||||||
|
CONFIG_PREFIX = "acl"
|
||||||
|
|
||||||
def _config_users(name):
|
def _config_users(name):
|
||||||
"""Get an iterable of strings from the configuraton string [acl] ``name``.
|
"""Get an iterable of strings from the configuraton string [acl] ``name``.
|
||||||
@ -40,18 +41,17 @@ def _config_users(name):
|
|||||||
stripped at the beginning and at the end of the values.
|
stripped at the beginning and at the end of the values.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for user in config.get("acl", name).split(","):
|
for user in config.get(CONFIG_PREFIX, name).split(","):
|
||||||
user = user.strip()
|
user = user.strip()
|
||||||
yield None if user == "None" else user
|
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("acl", "type")
|
acl_type = config.get(CONFIG_PREFIX, "type")
|
||||||
|
log.LOGGER.debug("acl_type = " + acl_type)
|
||||||
if acl_type == "None":
|
if acl_type == "None":
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
PUBLIC_USERS.extend(_config_users("public_users"))
|
|
||||||
PRIVATE_USERS.extend(_config_users("private_users"))
|
|
||||||
module = __import__("acl.%s" % acl_type, globals=globals(), level=2)
|
module = __import__("acl.%s" % acl_type, globals=globals(), level=2)
|
||||||
return getattr(module, acl_type)
|
return getattr(module, acl_type)
|
||||||
|
@ -29,14 +29,11 @@ from radicale import acl, config, log
|
|||||||
COURIER_SOCKET = config.get("acl", "courier_socket")
|
COURIER_SOCKET = config.get("acl", "courier_socket")
|
||||||
|
|
||||||
|
|
||||||
def has_right(owner, user, password):
|
def is_authenticated(user, password):
|
||||||
"""Check if ``user``/``password`` couple is valid."""
|
"""Check if ``user``/``password`` couple is valid."""
|
||||||
if not user or (owner not in acl.PRIVATE_USERS and user != owner):
|
|
||||||
# No user given, or owner is not private and is not user, forbidden
|
|
||||||
return False
|
|
||||||
|
|
||||||
line = "%s\nlogin\n%s\n%s" % (sys.argv[0], user, password)
|
line = "%s\nlogin\n%s\n%s" % (sys.argv[0], user, password)
|
||||||
line = "AUTH %i\n%s" % (len(line), line)
|
line = "%i\n%s" % (len(line), line)
|
||||||
try:
|
try:
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
sock.connect(COURIER_SOCKET)
|
sock.connect(COURIER_SOCKET)
|
||||||
@ -51,13 +48,7 @@ def has_right(owner, user, password):
|
|||||||
|
|
||||||
log.LOGGER.debug("Got Courier socket response: %r" % data)
|
log.LOGGER.debug("Got Courier socket response: %r" % data)
|
||||||
|
|
||||||
# Address, HOME, GID, and either UID or USERNAME are mandatory in resposne
|
if repr(data) == "FAIL":
|
||||||
# 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 courier authlib
|
|
||||||
# see http://www.courier-mta.org/authlib/README_authlib.html#authpipeproto
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
@ -58,11 +58,11 @@ def _sha1(hash_value, password):
|
|||||||
return sha1.digest() == base64.b64decode(hash_value)
|
return sha1.digest() == base64.b64decode(hash_value)
|
||||||
|
|
||||||
|
|
||||||
def has_right(owner, user, password):
|
def is_authenticated(user, password):
|
||||||
"""Check if ``user``/``password`` couple is valid."""
|
"""Check if ``user``/``password`` couple is valid."""
|
||||||
for line in open(FILENAME).readlines():
|
for line in open(FILENAME).readlines():
|
||||||
if line.strip():
|
if line.strip():
|
||||||
login, hash_value = line.strip().split(":")
|
login, hash_value = line.strip().split(":")
|
||||||
if login == user and (owner in acl.PRIVATE_USERS or owner == user):
|
if login == user:
|
||||||
return globals()["_%s" % ENCRYPTION](hash_value, password)
|
return globals()["_%s" % ENCRYPTION](hash_value, password)
|
||||||
return False
|
return False
|
||||||
|
76
radicale/authorization/__init__.py
Normal file
76
radicale/authorization/__init__.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# -*- 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)
|
||||||
|
|
||||||
|
|
47
radicale/authorization/allauthenticated.py
Normal file
47
radicale/authorization/allauthenticated.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# -*- 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 here is that all authenticated users
|
||||||
|
have read and write access to all collections.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from radicale import authorization, config, log
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def read_authorized(user, collection):
|
||||||
|
"""Check if the user is allowed to read the collection"""
|
||||||
|
log.LOGGER.debug("read_authorized '" + user + "' in '" + collection.name + "'");
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def write_authorized(user, collection):
|
||||||
|
"""Check if the user is allowed to write the collection"""
|
||||||
|
log.LOGGER.debug("write_authorized '" + user + "' in '" + collection.name + "'");
|
||||||
|
return True
|
||||||
|
|
49
radicale/authorization/owneronly.py
Normal file
49
radicale/authorization/owneronly.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# -*- 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 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):
|
||||||
|
"""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
|
||||||
|
|
||||||
|
|
||||||
|
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 + "'");
|
||||||
|
|
||||||
|
return user == collection.owner
|
||||||
|
|
65
radicale/authorization/static.py
Normal file
65
radicale/authorization/static.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# -*- 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
|
||||||
|
|
@ -53,10 +53,8 @@ INITIAL_CONFIG = {
|
|||||||
"type": "None",
|
"type": "None",
|
||||||
"public_users": "public",
|
"public_users": "public",
|
||||||
"private_users": "private",
|
"private_users": "private",
|
||||||
"htpasswd_filename": "/etc/radicale/users",
|
"httpasswd_filename": "/etc/radicale/users",
|
||||||
"htpasswd_encryption": "crypt",
|
"httpasswd_encryption": "crypt",
|
||||||
"imap_auth_host_name": "localhost",
|
|
||||||
"imap_auth_host_port": "143",
|
|
||||||
"ldap_url": "ldap://localhost:389/",
|
"ldap_url": "ldap://localhost:389/",
|
||||||
"ldap_base": "ou=users,dc=example,dc=com",
|
"ldap_base": "ou=users,dc=example,dc=com",
|
||||||
"ldap_attribute": "uid",
|
"ldap_attribute": "uid",
|
||||||
|
@ -35,7 +35,7 @@ except ImportError:
|
|||||||
import re
|
import re
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
from radicale import client, config, ical
|
from radicale import client, config, ical, access
|
||||||
|
|
||||||
|
|
||||||
NAMESPACES = {
|
NAMESPACES = {
|
||||||
@ -200,6 +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):
|
||||||
response = _propfind_response(path, collection, props, user)
|
response = _propfind_response(path, collection, props, user)
|
||||||
multistatus.append(response)
|
multistatus.append(response)
|
||||||
|
|
||||||
@ -283,13 +284,11 @@ def _propfind_response(path, item, props, user):
|
|||||||
if item.is_principal:
|
if item.is_principal:
|
||||||
tag = ET.Element(_tag("D", "principal"))
|
tag = ET.Element(_tag("D", "principal"))
|
||||||
element.append(tag)
|
element.append(tag)
|
||||||
if item.is_leaf(item.path) or (
|
if item.is_leaf(item.path):
|
||||||
not item.exists and item.resource_type):
|
tag = ET.Element(_tag("C", item.resource_type))
|
||||||
# 2nd case happens when the collection is not stored yet,
|
element.append(tag)
|
||||||
# but the resource type is guessed
|
if not item.exists and item.resource_type:
|
||||||
if item.resource_type == "addressbook":
|
# Collection not stored yet, but guessed resource type
|
||||||
tag = ET.Element(_tag("CR", item.resource_type))
|
|
||||||
else:
|
|
||||||
tag = ET.Element(_tag("C", item.resource_type))
|
tag = ET.Element(_tag("C", item.resource_type))
|
||||||
element.append(tag)
|
element.append(tag)
|
||||||
tag = ET.Element(_tag("D", "collection"))
|
tag = ET.Element(_tag("D", "collection"))
|
||||||
@ -301,8 +300,6 @@ def _propfind_response(path, item, props, user):
|
|||||||
elif tag == _tag("C", "calendar-timezone"):
|
elif tag == _tag("C", "calendar-timezone"):
|
||||||
element.text = ical.serialize(
|
element.text = ical.serialize(
|
||||||
item.tag, item.headers, item.timezones)
|
item.tag, item.headers, item.timezones)
|
||||||
elif tag == _tag("D", "displayname"):
|
|
||||||
element.text = item.name
|
|
||||||
else:
|
else:
|
||||||
human_tag = _tag_from_clark(tag)
|
human_tag = _tag_from_clark(tag)
|
||||||
if human_tag in collection_props:
|
if human_tag in collection_props:
|
||||||
@ -434,15 +431,8 @@ def report(path, xml_request, collection):
|
|||||||
in root.findall(_tag("D", "href")))
|
in root.findall(_tag("D", "href")))
|
||||||
else:
|
else:
|
||||||
hreferences = (path,)
|
hreferences = (path,)
|
||||||
# TODO: handle other filters
|
|
||||||
# TODO: handle the nested comp-filters correctly
|
|
||||||
# Read rfc4791-9.7.1 for info
|
|
||||||
tag_filters = set(
|
|
||||||
element.get("name") for element
|
|
||||||
in root.findall(".//%s" % _tag("C", "comp-filter")))
|
|
||||||
else:
|
else:
|
||||||
hreferences = ()
|
hreferences = ()
|
||||||
tag_filters = None
|
|
||||||
|
|
||||||
# Writing answer
|
# Writing answer
|
||||||
multistatus = ET.Element(_tag("D", "multistatus"))
|
multistatus = ET.Element(_tag("D", "multistatus"))
|
||||||
@ -465,9 +455,6 @@ def report(path, xml_request, collection):
|
|||||||
items = collection.components
|
items = collection.components
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
if tag_filters and item.tag not in tag_filters:
|
|
||||||
continue
|
|
||||||
|
|
||||||
response = ET.Element(_tag("D", "response"))
|
response = ET.Element(_tag("D", "response"))
|
||||||
multistatus.append(response)
|
multistatus.append(response)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user