summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG11
-rw-r--r--changes/feature_use-raw-sqlcipher-key-with-scrypt-as-kdf1
-rw-r--r--debian/changelog6
-rw-r--r--debian/soledad-server.init14
-rw-r--r--soledad/setup.py2
-rw-r--r--soledad/src/leap/soledad/auth.py3
-rw-r--r--soledad/src/leap/soledad/target.py9
-rw-r--r--soledad/src/leap/soledad/tests/test_server.py17
-rw-r--r--soledad/src/leap/soledad/tests/test_target.py20
-rw-r--r--soledad_server/pkg/soledad72
-rw-r--r--soledad_server/setup.py8
-rw-r--r--soledad_server/src/leap/soledad_server/__init__.py274
-rw-r--r--soledad_server/src/leap/soledad_server/auth.py431
13 files changed, 559 insertions, 309 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 93ce5071..3338d5b6 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,14 @@
+0.2.3 Jul 26:
+Client:
+ o Avoid possible timing attack in document's mac comparison by
+ comparing hashes instead of plain macs. Closes #3243.
+Server:
+ o Refactor server side auth classes to make it possible for other
+ kinds of authentication to be easily implemented. Closes #2621.
+ o Fix double specified /etc/leap/soledad-server.pem in initscript by
+ pointing the PRIVKEY_PATH to /etc/leap/soledad-server.key. Fixes
+ #3174.
+
0.2.2 Jul 12:
Client:
o Add method for password change.
diff --git a/changes/feature_use-raw-sqlcipher-key-with-scrypt-as-kdf b/changes/feature_use-raw-sqlcipher-key-with-scrypt-as-kdf
deleted file mode 100644
index 385c1c84..00000000
--- a/changes/feature_use-raw-sqlcipher-key-with-scrypt-as-kdf
+++ /dev/null
@@ -1 +0,0 @@
- o Use scrypt to derive the key for local encryption.
diff --git a/debian/changelog b/debian/changelog
index 1c65495c..b13f6c2a 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+soledad (0.2.3) unstable; urgency=low
+
+ * Upgrade to new release
+
+ -- Micah Anderson <micah@debian.org> Mon, 29 Jul 2013 20:47:19 -0400
+
soledad (0.2.2.1) unstable; urgency=low
* Fix installation of initscript
diff --git a/debian/soledad-server.init b/debian/soledad-server.init
index b1d898cc..d8df7c5d 100644
--- a/debian/soledad-server.init
+++ b/debian/soledad-server.init
@@ -15,7 +15,6 @@ RUNDIR=/var/lib/soledad/
OBJ=leap.soledad_server.application
LOGFILE=/var/log/soledad.log
HTTPS_PORT=2424
-PLAIN_PORT=65534
CERT_PATH=/etc/leap/soledad-server.pem
PRIVKEY_PATH=/etc/leap/soledad-server.pem
TWISTD_PATH=/usr/bin/twistd
@@ -32,14 +31,11 @@ case "$1" in
start)
echo -n "Starting soledad: twistd"
start-stop-daemon --start --quiet --exec $TWISTD_PATH -- \
- --pidfile=$PIDFILE \
- --logfile=$LOGFILE \
- web \
- --wsgi=$OBJ \
- --https=$HTTPS_PORT \
- --certificate=$CERT_PATH \
- --privkey=$PRIVKEY_PATH \
- --port=$PLAIN_PORT
+ --pidfile=$PIDFILE \
+ --logfile=$LOGFILE \
+ web \
+ --wsgi=$OBJ \
+ --port=ssl:$HTTPS_PORT:privateKey=$PRIVKEY_PATH:certKey=$CERT_PATH
echo "."
;;
diff --git a/soledad/setup.py b/soledad/setup.py
index 747b02bd..3e46c353 100644
--- a/soledad/setup.py
+++ b/soledad/setup.py
@@ -62,7 +62,7 @@ trove_classifiers = (
setup(
name='leap.soledad',
- version='0.2.2',
+ version='0.2.3',
url='https://leap.se/',
license='GPLv3+',
description='Synchronization of locally encrypted data among devices.',
diff --git a/soledad/src/leap/soledad/auth.py b/soledad/src/leap/soledad/auth.py
index 8c093099..81e838d2 100644
--- a/soledad/src/leap/soledad/auth.py
+++ b/soledad/src/leap/soledad/auth.py
@@ -24,7 +24,6 @@ they can do token-based auth requests to the Soledad server.
"""
-from u1db.remote.http_client import HTTPClientBase
from u1db import errors
@@ -68,4 +67,4 @@ class TokenBasedAuth(object):
return [('Authorization', 'Token %s' % auth.encode('base64')[:-1])]
else:
raise errors.UnknownAuthMethod(
- 'Wrong credentials: %s' % self._creds)
+ 'Wrong credentials: %s' % self._creds) \ No newline at end of file
diff --git a/soledad/src/leap/soledad/target.py b/soledad/src/leap/soledad/target.py
index 8b7aa8c7..9fac9f54 100644
--- a/soledad/src/leap/soledad/target.py
+++ b/soledad/src/leap/soledad/target.py
@@ -231,7 +231,14 @@ def decrypt_doc(crypto, doc):
crypto, doc.doc_id, doc.rev,
ciphertext,
doc.content[MAC_METHOD_KEY])
- if binascii.a2b_hex(doc.content[MAC_KEY]) != mac: # mac is stored as hex.
+ # we compare mac's hashes to avoid possible timing attacks that might
+ # exploit python's builtin comparison operator behaviour, which fails
+ # immediatelly when non-matching bytes are found.
+ doc_mac_hash = hashlib.sha256(
+ binascii.a2b_hex( # the mac is stored as hex
+ doc.content[MAC_KEY])).digest()
+ calculated_mac_hash = hashlib.sha256(mac).digest()
+ if doc_mac_hash != calculated_mac_hash:
raise WrongMac('Could not authenticate document\'s contents.')
# decrypt doc's content
enc_scheme = doc.content[ENC_SCHEME_KEY]
diff --git a/soledad/src/leap/soledad/tests/test_server.py b/soledad/src/leap/soledad/tests/test_server.py
index 490d2fc8..24cd68dc 100644
--- a/soledad/src/leap/soledad/tests/test_server.py
+++ b/soledad/src/leap/soledad/tests/test_server.py
@@ -21,19 +21,14 @@ Tests for server-related functionality.
"""
import os
-import shutil
import tempfile
import simplejson as json
-import hashlib
import mock
from leap.soledad import Soledad
-from leap.soledad_server import (
- SoledadApp,
- SoledadAuthMiddleware,
- URLToAuth,
-)
+from leap.soledad_server import SoledadApp
+from leap.soledad_server.auth import URLToAuthorization
from leap.soledad_server.couch import (
CouchServerState,
CouchDatabase,
@@ -42,11 +37,9 @@ from leap.soledad import target
from leap.common.testing.basetest import BaseLeapTest
-from leap.soledad.tests import ADDRESS
from leap.soledad.tests.u1db_tests import (
TestCaseWithServer,
simple_doc,
- nested_doc,
)
from leap.soledad.tests.test_couch import CouchDBTestCase
from leap.soledad.tests.test_target import (
@@ -93,7 +86,8 @@ class ServerAuthorizationTestCase(BaseLeapTest):
/user-db/sync-from/{source} | GET, PUT, POST
"""
uuid = 'myuuid'
- authmap = URLToAuth(uuid)
+ authmap = URLToAuthorization(
+ uuid, SoledadApp.SHARED_DB_NAME, SoledadApp.USER_DB_PREFIX)
dbname = authmap._uuid_dbname(uuid)
# test global auth
self.assertTrue(
@@ -208,7 +202,8 @@ class ServerAuthorizationTestCase(BaseLeapTest):
Test if authorization fails for a wrong dbname.
"""
uuid = 'myuuid'
- authmap = URLToAuth(uuid)
+ authmap = URLToAuthorization(
+ uuid, SoledadApp.SHARED_DB_NAME, SoledadApp.USER_DB_PREFIX)
dbname = 'somedb'
# test wrong-db database resource auth
self.assertFalse(
diff --git a/soledad/src/leap/soledad/tests/test_target.py b/soledad/src/leap/soledad/tests/test_target.py
index 73c9fe68..ca2878a5 100644
--- a/soledad/src/leap/soledad/tests/test_target.py
+++ b/soledad/src/leap/soledad/tests/test_target.py
@@ -40,10 +40,8 @@ from leap.soledad import (
auth,
)
from leap.soledad.document import SoledadDocument
-from leap.soledad_server import (
- SoledadApp,
- SoledadAuthMiddleware,
-)
+from leap.soledad_server import SoledadApp
+from leap.soledad_server.auth import SoledadTokenAuthMiddleware
from leap.soledad.tests import u1db_tests as tests
@@ -74,18 +72,18 @@ def make_soledad_app(state):
def make_token_soledad_app(state):
app = SoledadApp(state)
- def verify_token(environ, uuid, token):
- if uuid == 'user-uuid' and token == 'auth-token':
+ def _verify_authentication_data(uuid, auth_data):
+ if uuid == 'user-uuid' and auth_data == 'auth-token':
return True
return False
# we test for action authorization in leap.soledad.tests.test_server
- def verify_action(environ, uuid):
+ def _verify_authorization(uuid, environ):
return True
- application = SoledadAuthMiddleware(app)
- application.verify_token = verify_token
- application.verify_action = verify_action
+ application = SoledadTokenAuthMiddleware(app)
+ application._verify_authentication_data = _verify_authentication_data
+ application._verify_authorization = _verify_authorization
return application
@@ -190,7 +188,7 @@ class TestSoledadClientBase(test_http_client.TestHTTPClientBase):
return res
# mime solead application here.
if '/token' in environ['PATH_INFO']:
- auth = environ.get(SoledadAuthMiddleware.HTTP_AUTH_KEY)
+ auth = environ.get(SoledadTokenAuthMiddleware.HTTP_AUTH_KEY)
if not auth:
start_response("401 Unauthorized",
[('Content-Type', 'application/json')])
diff --git a/soledad_server/pkg/soledad b/soledad_server/pkg/soledad
new file mode 100644
index 00000000..1254cabe
--- /dev/null
+++ b/soledad_server/pkg/soledad
@@ -0,0 +1,72 @@
+#!/bin/sh
+### BEGIN INIT INFO
+# Provides: soledad
+# Required-Start: $network $named $remote_fs $syslog $time
+# Required-Stop: $network $named $remote_fs $syslog
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: Start soledad daemon at boot time
+# Description: Synchronization of locally encrypted data among devices
+### END INIT INFO
+
+PATH=/sbin:/bin:/usr/sbin:/usr/bin
+PIDFILE=/var/run/soledad.pid
+RUNDIR=/var/lib/soledad/
+OBJ=leap.soledad_server.application
+LOGFILE=/var/log/soledad.log
+HTTPS_PORT=2424
+PLAIN_PORT=65534
+CERT_PATH=/etc/leap/soledad-server.pem
+PRIVKEY_PATH=/etc/leap/soledad-server.key
+TWISTD_PATH=/usr/bin/twistd
+HOME=/var/lib/soledad/
+
+[ -r /etc/default/soledad ] && . /etc/default/soledad
+
+test -r /etc/leap/ || exit 0
+
+. /lib/lsb/init-functions
+
+
+case "$1" in
+ start)
+ echo -n "Starting soledad: twistd"
+ start-stop-daemon --start --quiet --exec $TWISTD_PATH -- \
+ --pidfile=$PIDFILE \
+ --logfile=$LOGFILE \
+ web \
+ --wsgi=$OBJ \
+ --https=$HTTPS_PORT \
+ --certificate=$CERT_PATH \
+ --privkey=$PRIVKEY_PATH \
+ --port=$PLAIN_PORT
+ echo "."
+ ;;
+
+ stop)
+ echo -n "Stopping soledad: twistd"
+ start-stop-daemon --stop --quiet \
+ --pidfile $PIDFILE
+ echo "."
+ ;;
+
+ restart)
+ $0 stop
+ $0 start
+ ;;
+
+ force-reload)
+ $0 restart
+ ;;
+
+ status)
+ status_of_proc -p $PIDFILE $TWISTD_PATH soledad && exit 0 || exit $?
+ ;;
+
+ *)
+ echo "Usage: /etc/init.d/soledad {start|stop|restart|force-reload|status}" >&2
+ exit 1
+ ;;
+esac
+
+exit 0
diff --git a/soledad_server/setup.py b/soledad_server/setup.py
index c33526ca..f022c9cf 100644
--- a/soledad_server/setup.py
+++ b/soledad_server/setup.py
@@ -34,12 +34,15 @@ install_requirements = [
'six==1.1.0',
'routes',
'PyOpenSSL',
- 'leap.soledad>=0.2.1',
+ 'leap.soledad>=0.2.3',
]
if os.environ.get('VIRTUAL_ENV', None):
data_files = None
+else:
+ # XXX this should go only for linux/mac
+ data_files = [("/etc/init.d/", ["pkg/soledad"])]
trove_classifiers = (
"Development Status :: 3 - Alpha",
@@ -57,7 +60,7 @@ trove_classifiers = (
setup(
name='leap.soledad_server',
- version='0.2.2',
+ version='0.2.3',
url='https://leap.se/',
license='GPLv3+',
description='Synchronization of locally encrypted data among devices.',
@@ -72,5 +75,6 @@ setup(
packages=find_packages('src'),
package_dir={'': 'src'},
install_requires=install_requirements,
+ data_files=data_files,
classifiers=trove_classifiers,
)
diff --git a/soledad_server/src/leap/soledad_server/__init__.py b/soledad_server/src/leap/soledad_server/__init__.py
index bea5d5fd..79609d7d 100644
--- a/soledad_server/src/leap/soledad_server/__init__.py
+++ b/soledad_server/src/leap/soledad_server/__init__.py
@@ -24,13 +24,7 @@ This should be run with:
"""
import configparser
-import httplib
-import simplejson as json
-
-from hashlib import sha256
-from routes.mapper import Mapper
-from u1db import DBNAME_CONSTRAINTS
from u1db.remote import http_app
@@ -41,8 +35,6 @@ old_tsafe = tsafe
from twisted.web.wsgi import WSGIResource
from twisted.internet import reactor
-from twisted.python import log
-
from twisted import version
if version.base() == "12.0.0":
# Put OpenSSL's tsafe back into place. This can probably be removed if we
@@ -50,272 +42,12 @@ if version.base() == "12.0.0":
import sys
sys.modules['OpenSSL.tsafe'] = old_tsafe
-from couchdb.client import Server
-from leap.soledad import SECRETS_DOC_ID_HASH_PREFIX
+from leap.soledad_server.auth import SoledadTokenAuthMiddleware
from leap.soledad_server.couch import CouchServerState
#-----------------------------------------------------------------------------
-# Authentication
-#-----------------------------------------------------------------------------
-
-class Unauthorized(Exception):
- """
- User authentication failed.
- """
-
-
-class URLToAuth(object):
- """
- Verify if actions can be performed by a user.
- """
-
- HTTP_METHOD_GET = 'GET'
- HTTP_METHOD_PUT = 'PUT'
- HTTP_METHOD_DELETE = 'DELETE'
- HTTP_METHOD_POST = 'POST'
-
- def __init__(self, uuid):
- """
- Initialize the mapper.
-
- The C{uuid} is used to create the rules that will either allow or
- disallow the user to perform specific actions.
-
- @param uuid: The user uuid.
- @type uuid: str
- """
- self._map = Mapper(controller_scan=None)
- self._register_auth_info(self._uuid_dbname(uuid))
-
- def is_authorized(self, environ):
- """
- Return whether an HTTP request that produced the CGI C{environ}
- corresponds to an authorized action.
-
- @param environ: Dictionary containing CGI variables.
- @type environ: dict
-
- @return: Whether the action is authorized or not.
- @rtype: bool
- """
- return self._map.match(environ=environ) is not None
-
- def _register(self, pattern, http_methods):
- """
- Register a C{pattern} in the mapper as valid for C{http_methods}.
-
- @param pattern: The URL pattern that corresponds to the user action.
- @type pattern: str
- @param http_methods: A list of authorized HTTP methods.
- @type http_methods: list of str
- """
- self._map.connect(
- None, pattern, http_methods=http_methods,
- conditions=dict(method=http_methods),
- requirements={'dbname': DBNAME_CONSTRAINTS})
-
- def _uuid_dbname(self, uuid):
- """
- Return the database name corresponding to C{uuid}.
-
- @param uuid: The user uid.
- @type uuid: str
-
- @return: The database name corresponding to C{uuid}.
- @rtype: str
- """
- return '%s%s' % (SoledadApp.USER_DB_PREFIX, uuid)
-
- def _register_auth_info(self, dbname):
- """
- Register the authorization info in the mapper using C{dbname} as the
- user's database name.
-
- This method sets up the following authorization rules:
-
- URL path | Authorized actions
- --------------------------------------------------
- / | GET
- /shared-db | GET
- /shared-db/docs | -
- /shared-db/doc/{id} | GET, PUT, DELETE
- /shared-db/sync-from/{source} | -
- /user-db | GET, PUT, DELETE
- /user-db/docs | -
- /user-db/doc/{id} | -
- /user-db/sync-from/{source} | GET, PUT, POST
-
- @param dbname: The name of the user's database.
- @type dbname: str
- """
- # auth info for global resource
- self._register('/', [self.HTTP_METHOD_GET])
- # auth info for shared-db database resource
- self._register(
- '/%s' % SoledadApp.SHARED_DB_NAME,
- [self.HTTP_METHOD_GET])
- # auth info for shared-db doc resource
- self._register(
- '/%s/doc/{id:.*}' % SoledadApp.SHARED_DB_NAME,
- [self.HTTP_METHOD_GET, self.HTTP_METHOD_PUT,
- self.HTTP_METHOD_DELETE])
- # auth info for user-db database resource
- self._register(
- '/%s' % dbname,
- [self.HTTP_METHOD_GET, self.HTTP_METHOD_PUT,
- self.HTTP_METHOD_DELETE])
- # auth info for user-db sync resource
- self._register(
- '/%s/sync-from/{source_replica_uid}' % dbname,
- [self.HTTP_METHOD_GET, self.HTTP_METHOD_PUT,
- self.HTTP_METHOD_POST])
- # generate the regular expressions
- self._map.create_regs()
-
-
-class SoledadAuthMiddleware(object):
- """
- Soledad Authentication WSGI middleware.
-
- In general, databases are accessed using a token provided by the LEAP API.
- Some special databases can be read without authentication.
- """
-
- TOKENS_DB = "tokens"
- TOKENS_TYPE_KEY = "type"
- TOKENS_TYPE_DEF = "Token"
- TOKENS_USER_ID_KEY = "user_id"
-
- HTTP_AUTH_KEY = "HTTP_AUTHORIZATION"
- PATH_INFO_KEY = "PATH_INFO"
-
- CONTENT_TYPE_JSON = ('content-type', 'application/json')
-
- def __init__(self, app):
- """
- Initialize the Soledad Authentication Middleware.
-
- @param app: The application to run on successfull authentication.
- @type app: u1db.remote.http_app.HTTPApp
- @param prefix: Auth app path prefix.
- @type prefix: str
- """
- self._app = app
-
- def _error(self, start_response, status, description, message=None):
- """
- Send a JSON serialized error to WSGI client.
-
- @param start_response: Callable of the form start_response(status,
- response_headers, exc_info=None).
- @type start_response: callable
- @param status: Status string of the form "999 Message here"
- @type status: str
- @param response_headers: A list of (header_name, header_value) tuples
- describing the HTTP response header.
- @type response_headers: list
- @param description: The error description.
- @type description: str
- @param message: The error message.
- @type message: str
-
- @return: List with JSON serialized error message.
- @rtype list
- """
- start_response("%d %s" % (status, httplib.responses[status]),
- [self.CONTENT_TYPE_JSON])
- err = {"error": description}
- if message:
- err['message'] = message
- return [json.dumps(err)]
-
- def __call__(self, environ, start_response):
- """
- Handle a WSGI call to the authentication application.
-
- @param environ: Dictionary containing CGI variables.
- @type environ: dict
- @param start_response: Callable of the form start_response(status,
- response_headers, exc_info=None).
- @type start_response: callable
-
- @return: Target application results if authentication succeeds, an
- error message otherwise.
- @rtype: list
- """
- unauth_err = lambda msg: self._error(start_response,
- 401,
- "unauthorized",
- msg)
-
- auth = environ.get(self.HTTP_AUTH_KEY)
- if not auth:
- return unauth_err("Missing Token Authentication.")
-
- scheme, encoded = auth.split(None, 1)
- if scheme.lower() != 'token':
- return unauth_err("Missing Token Authentication")
-
- uuid, token = encoded.decode('base64').split(':', 1)
- if not self.verify_token(environ, uuid, token):
- return unauth_err("Incorrect address or token.")
-
- if not self.verify_action(uuid, environ):
- return unauth_err("Unauthorized action.")
-
- del environ[self.HTTP_AUTH_KEY]
-
- return self._app(environ, start_response)
-
- def verify_token(self, environ, uuid, token):
- """
- Verify if token is valid for authenticating this request.
-
- @param environ: Dictionary containing CGI variables.
- @type environ: dict
- @param uuid: The user's uuid.
- @type uuid: str
- @param token: The authentication token.
- @type token: str
-
- @return: Whether the token is valid for authenticating the request.
- @rtype: bool
- """
-
- server = Server(url=self._app.state.couch_url)
- try:
- dbname = self.TOKENS_DB
- db = server[dbname]
- token = db.get(token)
- if token is None:
- return False
- return token[self.TOKENS_TYPE_KEY] == self.TOKENS_TYPE_DEF and \
- token[self.TOKENS_USER_ID_KEY] == uuid
- except Exception as e:
- log.err(e)
- return False
- return True
-
- def verify_action(self, uuid, environ):
- """
- Verify if the user is authorized to perform the requested action over
- the requested database.
-
- @param uuid: The user's uuid.
- @type uuid: str
- @param environ: Dictionary containing CGI variables.
- @type environ: dict
-
- @return: Whether the user is authorize to perform the requested action
- over the requested db.
- @rtype: bool
- """
- return URLToAuth(uuid).is_authorized(environ)
-
-
-#-----------------------------------------------------------------------------
# Soledad WSGI application
#-----------------------------------------------------------------------------
@@ -329,7 +61,7 @@ class SoledadApp(http_app.HTTPApp):
The name of the shared database that holds user's encrypted secrets.
"""
- USER_DB_PREFIX = 'uuid-'
+ USER_DB_PREFIX = 'user-'
"""
The string prefix of users' databases.
"""
@@ -388,6 +120,6 @@ conf = load_configuration('/etc/leap/soledad-server.conf')
state = CouchServerState(conf['couch_url'])
# WSGI application that may be used by `twistd -web`
-application = SoledadAuthMiddleware(SoledadApp(state))
+application = SoledadTokenAuthMiddleware(SoledadApp(state))
resource = WSGIResource(reactor, reactor.getThreadPool(), application)
diff --git a/soledad_server/src/leap/soledad_server/auth.py b/soledad_server/src/leap/soledad_server/auth.py
new file mode 100644
index 00000000..e0169523
--- /dev/null
+++ b/soledad_server/src/leap/soledad_server/auth.py
@@ -0,0 +1,431 @@
+# -*- coding: utf-8 -*-
+# auth.py
+# Copyright (C) 2013 LEAP
+#
+# This program 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 program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+"""
+Authentication facilities for Soledad Server.
+"""
+
+
+import httplib
+import simplejson as json
+
+
+from u1db import DBNAME_CONSTRAINTS
+from abc import ABCMeta, abstractmethod
+from routes.mapper import Mapper
+from couchdb.client import Server
+from twisted.python import log
+
+
+#-----------------------------------------------------------------------------
+# Authentication
+#-----------------------------------------------------------------------------
+
+class Unauthorized(Exception):
+ """
+ User authentication failed.
+ """
+
+
+class URLToAuthorization(object):
+ """
+ Verify if actions can be performed by a user.
+ """
+
+ HTTP_METHOD_GET = 'GET'
+ HTTP_METHOD_PUT = 'PUT'
+ HTTP_METHOD_DELETE = 'DELETE'
+ HTTP_METHOD_POST = 'POST'
+
+ def __init__(self, uuid, shared_db_name, user_db_prefix):
+ """
+ Initialize the mapper.
+
+ The C{uuid} is used to create the rules that will either allow or
+ disallow the user to perform specific actions.
+
+ @param uuid: The user uuid.
+ @type uuid: str
+ @param shared_db_name: The name of the shared database that holds
+ user's encrypted secrets.
+ @type shared_db_name: str
+ @param user_db_prefix: The string prefix of users' databases.
+ @type user_db_prefix: str
+ """
+ self._map = Mapper(controller_scan=None)
+ self._user_db_prefix = user_db_prefix
+ self._shared_db_name = shared_db_name
+ self._register_auth_info(self._uuid_dbname(uuid))
+
+ def is_authorized(self, environ):
+ """
+ Return whether an HTTP request that produced the CGI C{environ}
+ corresponds to an authorized action.
+
+ @param environ: Dictionary containing CGI variables.
+ @type environ: dict
+
+ @return: Whether the action is authorized or not.
+ @rtype: bool
+ """
+ return self._map.match(environ=environ) is not None
+
+ def _register(self, pattern, http_methods):
+ """
+ Register a C{pattern} in the mapper as valid for C{http_methods}.
+
+ @param pattern: The URL pattern that corresponds to the user action.
+ @type pattern: str
+ @param http_methods: A list of authorized HTTP methods.
+ @type http_methods: list of str
+ """
+ self._map.connect(
+ None, pattern, http_methods=http_methods,
+ conditions=dict(method=http_methods),
+ requirements={'dbname': DBNAME_CONSTRAINTS})
+
+ def _uuid_dbname(self, uuid):
+ """
+ Return the database name corresponding to C{uuid}.
+
+ @param uuid: The user uid.
+ @type uuid: str
+
+ @return: The database name corresponding to C{uuid}.
+ @rtype: str
+ """
+ return '%s%s' % (self._user_db_prefix, uuid)
+
+ def _register_auth_info(self, dbname):
+ """
+ Register the authorization info in the mapper using C{dbname} as the
+ user's database name.
+
+ This method sets up the following authorization rules:
+
+ URL path | Authorized actions
+ --------------------------------------------------
+ / | GET
+ /shared-db | GET
+ /shared-db/docs | -
+ /shared-db/doc/{id} | GET, PUT, DELETE
+ /shared-db/sync-from/{source} | -
+ /user-db | GET, PUT, DELETE
+ /user-db/docs | -
+ /user-db/doc/{id} | -
+ /user-db/sync-from/{source} | GET, PUT, POST
+
+ @param dbname: The name of the user's database.
+ @type dbname: str
+ """
+ # auth info for global resource
+ self._register('/', [self.HTTP_METHOD_GET])
+ # auth info for shared-db database resource
+ self._register(
+ '/%s' % self._shared_db_name,
+ [self.HTTP_METHOD_GET])
+ # auth info for shared-db doc resource
+ self._register(
+ '/%s/doc/{id:.*}' % self._shared_db_name,
+ [self.HTTP_METHOD_GET, self.HTTP_METHOD_PUT,
+ self.HTTP_METHOD_DELETE])
+ # auth info for user-db database resource
+ self._register(
+ '/%s' % dbname,
+ [self.HTTP_METHOD_GET, self.HTTP_METHOD_PUT,
+ self.HTTP_METHOD_DELETE])
+ # auth info for user-db sync resource
+ self._register(
+ '/%s/sync-from/{source_replica_uid}' % dbname,
+ [self.HTTP_METHOD_GET, self.HTTP_METHOD_PUT,
+ self.HTTP_METHOD_POST])
+ # generate the regular expressions
+ self._map.create_regs()
+
+
+class SoledadAuthMiddleware(object):
+ """
+ Soledad Authentication WSGI middleware.
+
+ This class must be extended to implement specific authentication methods
+ (see SoledadTokenAuthMiddleware below).
+
+ It expects an HTTP_AUTHORIZATION header containing the the concatenation of
+ the following strings:
+
+ 1. The authentication scheme. It will be verified by the
+ _verify_authentication_scheme() method.
+
+ 2. A space character.
+
+ 3. The base64 encoded string of the concatenation of the user uuid with
+ the authentication data, separated by a collon, like this:
+
+ base64("<uuid>:<auth_data>")
+
+ After authentication check, the class performs an authorization check to
+ verify whether the user is authorized to perform the requested action.
+
+ On client-side, 2 methods must be implemented so the soledad client knows
+ how to send authentication headers to server:
+
+ * set_<method>_credentials: store authentication credentials in the
+ class.
+
+ * _sign_request: format and include custom authentication data in
+ the HTTP_AUTHORIZATION header.
+
+ See leap.soledad.auth and u1db.remote.http_client.HTTPClient to understand
+ how to do it.
+ """
+
+ __metaclass__ = ABCMeta
+
+ HTTP_AUTH_KEY = "HTTP_AUTHORIZATION"
+ PATH_INFO_KEY = "PATH_INFO"
+
+ CONTENT_TYPE_JSON = ('content-type', 'application/json')
+
+ def __init__(self, app):
+ """
+ Initialize the Soledad Authentication Middleware.
+
+ @param app: The application to run on successfull authentication.
+ @type app: u1db.remote.http_app.HTTPApp
+ @param prefix: Auth app path prefix.
+ @type prefix: str
+ """
+ self._app = app
+
+ def _error(self, start_response, status, description, message=None):
+ """
+ Send a JSON serialized error to WSGI client.
+
+ @param start_response: Callable of the form start_response(status,
+ response_headers, exc_info=None).
+ @type start_response: callable
+ @param status: Status string of the form "999 Message here"
+ @type status: str
+ @param response_headers: A list of (header_name, header_value) tuples
+ describing the HTTP response header.
+ @type response_headers: list
+ @param description: The error description.
+ @type description: str
+ @param message: The error message.
+ @type message: str
+
+ @return: List with JSON serialized error message.
+ @rtype list
+ """
+ start_response("%d %s" % (status, httplib.responses[status]),
+ [self.CONTENT_TYPE_JSON])
+ err = {"error": description}
+ if message:
+ err['message'] = message
+ return [json.dumps(err)]
+
+ def _unauthorized_error(self, start_response, message):
+ """
+ Send a unauth error.
+
+ @param message: The error message.
+ @type message: str
+ @param start_response: Callable of the form start_response(status,
+ response_headers, exc_info=None).
+ @type start_response: callable
+
+ @return: List with JSON serialized error message.
+ @rtype list
+ """
+ return self._error(
+ start_response,
+ 401,
+ "unauthorized",
+ message)
+
+ def __call__(self, environ, start_response):
+ """
+ Handle a WSGI call to the authentication application.
+
+ @param environ: Dictionary containing CGI variables.
+ @type environ: dict
+ @param start_response: Callable of the form start_response(status,
+ response_headers, exc_info=None).
+ @type start_response: callable
+
+ @return: Target application results if authentication succeeds, an
+ error message otherwise.
+ @rtype: list
+ """
+ # check for authentication header
+ auth = environ.get(self.HTTP_AUTH_KEY)
+ if not auth:
+ return self._unauthorized_error(
+ start_response, "Missing authentication header.")
+
+ # get authentication data
+ scheme, encoded = auth.split(None, 1)
+ uuid, auth_data = encoded.decode('base64').split(':', 1)
+ if not self._verify_authentication_scheme(scheme):
+ return self._unauthorized_error("Wrong authentication scheme")
+
+ # verify if user is athenticated
+ if not self._verify_authentication_data(uuid, auth_data):
+ return self._unauthorized_error(
+ start_response,
+ self._get_auth_error_string())
+
+ # verify if user is authorized to perform action
+ if not self._verify_authorization(environ, uuid):
+ return self._unauthorized_error(
+ start_response,
+ "Unauthorized action.")
+
+ # move on to the real Soledad app
+ del environ[self.HTTP_AUTH_KEY]
+ return self._app(environ, start_response)
+
+ @abstractmethod
+ def _verify_authentication_scheme(self, scheme):
+ """
+ Verify if authentication scheme is valid.
+
+ @param scheme: Auth scheme extracted from the HTTP_AUTHORIZATION
+ header.
+ @type scheme: str
+
+ @return: Whether the authentitcation scheme is valid.
+ """
+ return None
+
+ @abstractmethod
+ def _verify_authentication_data(self, uuid, auth_data):
+ """
+ Verify valid authenticatiion for this request.
+
+ @param uuid: The user's uuid.
+ @type uuid: str
+ @param auth_data: Authentication data.
+ @type auth_data: str
+
+ @return: Whether the token is valid for authenticating the request.
+ @rtype: bool
+ """
+ return None
+
+ def _verify_authorization(self, environ, uuid):
+ """
+ Verify if the user is authorized to perform the requested action over
+ the requested database.
+
+ @param environ: Dictionary containing CGI variables.
+ @type environ: dict
+ @param uuid: The user's uuid.
+ @type uuid: str
+
+ @return: Whether the user is authorize to perform the requested action
+ over the requested db.
+ @rtype: bool
+ """
+ return URLToAuthorization(
+ uuid, self.app.SHARED_DB_NAME,
+ self.app.USER_DB_PREFIX
+ ).is_authorized(environ)
+
+ @abstractmethod
+ def _get_auth_error_string(self):
+ """
+ Return an error string specific for each kind of authentication method.
+
+ @return: The error string.
+ """
+ return None
+
+
+class SoledadTokenAuthMiddleware(SoledadAuthMiddleware):
+ """
+ Token based authentication.
+ """
+
+ TOKENS_DB = "tokens"
+ TOKENS_TYPE_KEY = "type"
+ TOKENS_TYPE_DEF = "Token"
+ TOKENS_USER_ID_KEY = "user_id"
+
+ TOKEN_AUTH_ERROR_STRING = "Incorrect address or token."
+
+ def _verify_authentication_scheme(self, scheme):
+ """
+ Verify if authentication scheme is valid.
+
+ @param scheme: Auth scheme extracted from the HTTP_AUTHORIZATION
+ header.
+ @type scheme: str
+
+ @return: Whether the authentitcation scheme is valid.
+ """
+ if scheme.lower() != 'token':
+ return False
+ return True
+
+ def _verify_authentication_data(self, uuid, auth_data):
+ """
+ Extract token from C{auth_data} and proceed with verification of
+ C{uuid} authentication.
+
+ @param uuid: The user UID.
+ @type uuid: str
+ @param auth_data: Authentication data (i.e. the token).
+ @type auth_data: str
+
+ @return: Whether the token is valid for authenticating the request.
+ @rtype: bool
+ """
+ token = auth_data # we expect a cleartext token at this point
+ return self._verify_token_in_couchdb(uuid, token)
+
+ def _verify_token_in_couchdb(self, uuid, token):
+ """
+ Query couchdb to decide if C{token} is valid for C{uuid}.
+
+ @param uuid: The user uuid.
+ @type uuid: str
+ @param token: The token.
+ @type token: str
+ """
+ server = Server(url=self._app.state.couch_url)
+ try:
+ dbname = self.TOKENS_DB
+ db = server[dbname]
+ token = db.get(token)
+ if token is None:
+ return False
+ return token[self.TOKENS_TYPE_KEY] == self.TOKENS_TYPE_DEF and \
+ token[self.TOKENS_USER_ID_KEY] == uuid
+ except Exception as e:
+ log.err(e)
+ return False
+ return True
+
+ def _get_auth_error_string(self):
+ """
+ Get the error string for token auth.
+
+ @return: The error string.
+ """
+ return self.TOKEN_AUTH_ERROR_STRING