Merge branch 'authentication' of github.com:cristen/Radicale into authentication

This commit is contained in:
Jean-Marc Martins 2013-09-13 17:41:21 +02:00
commit 6c40f5e24a
8 changed files with 48 additions and 52 deletions

View File

@ -3,8 +3,8 @@
====== ======
0.8.1 - *Not released yet* 0.9 - *Not released yet*
========================== ========================
* 1-file-per-event storage (by Jean-Marc Martins) * 1-file-per-event storage (by Jean-Marc Martins)
* Git support for filesystem storages (by Jean-Marc Martins) * Git support for filesystem storages (by Jean-Marc Martins)

7
config
View File

@ -30,7 +30,7 @@ dns_lookup = True
# Root URL of Radicale (starting and ending with a slash) # Root URL of Radicale (starting and ending with a slash)
base_prefix = / base_prefix = /
# Message displayed in the client when a password is needed # Message displayed in the client when a password is needed
realm = Radicale - Password Required lol realm = Radicale - Password Required
[encoding] [encoding]
@ -45,11 +45,6 @@ stock = utf-8
# Value: None | htpasswd | IMAP | LDAP | PAM | courier | http # Value: None | htpasswd | IMAP | LDAP | PAM | courier | http
type = None type = None
# Usernames used for public collections, separated by a comma
public_users = public
# Usernames used for private collections, separated by a comma
private_users = private
# Htpasswd filename # Htpasswd filename
htpasswd_filename = /etc/radicale/users htpasswd_filename = /etc/radicale/users
# Htpasswd encryption method # Htpasswd encryption method

View File

@ -55,13 +55,6 @@ VERSION = "git"
# tries to access information they don't have rights to # tries to access information they don't have rights to
NOT_ALLOWED = (client.FORBIDDEN, {}, None) NOT_ALLOWED = (client.FORBIDDEN, {}, None)
# Standard "authenticate" response that is returned when a user tries to access
# non-public information w/o submitting proper authentication credentials
WRONG_CREDENTIALS = (
client.UNAUTHORIZED,
{"WWW-Authenticate": "Basic realm=\"%s\"" % config.get("server", "realm")},
None)
class HTTPServer(wsgiref.simple_server.WSGIServer, object): class HTTPServer(wsgiref.simple_server.WSGIServer, object):
"""HTTP server.""" """HTTP server."""
@ -285,25 +278,28 @@ class Application(object):
else: else:
user = password = None user = password = None
if not items or function == self.options or \
auth.is_authenticated(user, password):
read_allowed_items, write_allowed_items = \ read_allowed_items, write_allowed_items = \
self.collect_allowed_items(items, user) self.collect_allowed_items(items, user)
if read_allowed_items or write_allowed_items or \ if ((read_allowed_items or write_allowed_items)
and auth.is_authenticated(user, password)) or \
function == self.options or not items: function == self.options or not items:
# Collections found, or OPTIONS request, or no items at all # Collections found, or OPTIONS request, or no items at all
status, headers, answer = function( status, headers, answer = function(
environ, read_allowed_items, write_allowed_items, content, environ, read_allowed_items, write_allowed_items, content,
user) user)
else: else:
# Good user but has no rights to any of the given collections
status, headers, answer = NOT_ALLOWED status, headers, answer = NOT_ALLOWED
else:
if (status, headers, answer) == NOT_ALLOWED and \
not auth.is_authenticated(user, password):
# Unknown or unauthorized user # Unknown or unauthorized user
log.LOGGER.info("%s refused" % (user or "Anonymous user")) log.LOGGER.info("%s refused" % (user or "Anonymous user"))
status, headers, answer = WRONG_CREDENTIALS status = client.UNAUTHORIZED
headers = {
"WWW-Authenticate":
"Basic realm=\"%s\"" % config.get("server", "realm")}
answer = None
# Set content length # Set content length
if answer: if answer:

View File

@ -53,8 +53,6 @@ INITIAL_CONFIG = {
"stock": "utf-8"}, "stock": "utf-8"},
"auth": { "auth": {
"type": "None", "type": "None",
"public_users": "public",
"private_users": "private",
"htpasswd_filename": "/etc/radicale/users", "htpasswd_filename": "/etc/radicale/users",
"htpasswd_encryption": "crypt", "htpasswd_encryption": "crypt",
"imap_hostname": "localhost", "imap_hostname": "localhost",

View File

@ -50,8 +50,6 @@ except ImportError:
# pylint: enable=F0401 # pylint: enable=F0401
FILENAME = os.path.expanduser(config.get("rights", "file"))
TYPE = config.get("rights", "type").lower()
DEFINED_RIGHTS = { DEFINED_RIGHTS = {
"owner_write": "[r]\nuser:.*\ncollection:.*\npermission:r\n" "owner_write": "[r]\nuser:.*\ncollection:.*\npermission:r\n"
"[w]\nuser:.*\ncollection:^%(login)s/.+$\npermission:w", "[w]\nuser:.*\ncollection:^%(login)s/.+$\npermission:w",
@ -60,17 +58,19 @@ DEFINED_RIGHTS = {
def _read_from_sections(user, collection, permission): def _read_from_sections(user, collection, permission):
"""Get regex sections.""" """Get regex sections."""
filename = os.path.expanduser(config.get("rights", "file"))
rights_type = config.get("rights", "type").lower()
regex = ConfigParser({"login": user, "path": collection}) regex = ConfigParser({"login": user, "path": collection})
if TYPE in DEFINED_RIGHTS: if rights_type in DEFINED_RIGHTS:
log.LOGGER.debug("Rights type '%s'" % TYPE) log.LOGGER.debug("Rights type '%s'" % rights_type)
regex.readfp(io.BytesIO(DEFINED_RIGHTS[TYPE])) regex.readfp(io.BytesIO(DEFINED_RIGHTS[rights_type]))
elif TYPE == "from_file": elif rights_type == "from_file":
log.LOGGER.debug("Reading rights from file %s" % FILENAME) log.LOGGER.debug("Reading rights from file %s" % filename)
if not regex.read(FILENAME): if not regex.read(filename):
log.LOGGER.error("File '%s' not found for rights" % FILENAME) log.LOGGER.error("File '%s' not found for rights" % filename)
return False return False
else: else:
log.LOGGER.error("Unknown rights type '%s'" % TYPE) log.LOGGER.error("Unknown rights type '%s'" % rights_type)
return False return False
for section in regex.sections(): for section in regex.sections():
@ -91,6 +91,10 @@ def _read_from_sections(user, collection, permission):
def authorized(user, collection, right): def authorized(user, collection, right):
"""Check if the user is allowed to read or write the collection.""" """Check if the user is allowed to read or write the collection.
return TYPE == "none" or (user and _read_from_sections(
user, collection.url.rstrip("/") or "/", right)) If the user is empty it checks for anonymous rights
"""
rights_type = config.get("rights", "type").lower()
return rights_type == "none" or (_read_from_sections(
user or "", collection.url.rstrip("/") or "/", right))

View File

@ -78,10 +78,10 @@ class DBLine(Base):
"""Table of item's lines.""" """Table of item's lines."""
__tablename__ = "line" __tablename__ = "line"
key = Column(String, primary_key=True) key = Column(String)
value = Column(String) value = Column(String)
item_name = Column(String, ForeignKey("item.name"), primary_key=True) item_name = Column(String, ForeignKey("item.name"))
timestamp = Column(DateTime, default=datetime.now) timestamp = Column(DateTime, default=datetime.now, primary_key=True)
item = relationship( item = relationship(
"DBItem", backref="lines", order_by=timestamp) "DBItem", backref="lines", order_by=timestamp)
@ -116,7 +116,7 @@ class Collection(ical.Collection):
items = ( items = (
self.session.query(DBItem) self.session.query(DBItem)
.filter_by(collection_path=self.path, tag=item_type.tag) .filter_by(collection_path=self.path, tag=item_type.tag)
.order_by("name").all()) .order_by(DBItem.name).all())
for item in items: for item in items:
text = "\n".join( text = "\n".join(
"%s:%s" % (line.key, line.value) for line in item.lines) "%s:%s" % (line.key, line.value) for line in item.lines)
@ -189,7 +189,7 @@ class Collection(ical.Collection):
headers = ( headers = (
self.session.query(DBHeader) self.session.query(DBHeader)
.filter_by(collection_path=self.path) .filter_by(collection_path=self.path)
.order_by("key").all()) .order_by(DBHeader.key).all())
return [ return [
ical.Header("%s:%s" % (header.key, header.value)) ical.Header("%s:%s" % (header.key, header.value))
for header in headers] for header in headers]

View File

@ -19,8 +19,7 @@ create table line (
key varchar not null, key varchar not null,
value varchar not null, value varchar not null,
item_name varchar references item (name) not null, item_name varchar references item (name) not null,
timestamp timestamp not null, timestamp timestamp not null);
primary key (key, value, item_name, timestamp));
create table property ( create table property (
key varchar not null, key varchar not null,

View File

@ -33,6 +33,10 @@ from io import BytesIO
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
import radicale import radicale
os.environ["RADICALE_CONFIG"] = os.path.join(os.path.dirname(
os.path.dirname(__file__)), "config")
from radicale import config from radicale import config
from radicale.auth import htpasswd from radicale.auth import htpasswd
from radicale.storage import filesystem, database from radicale.storage import filesystem, database