summaryrefslogtreecommitdiff
path: root/src/leap/soledad
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/soledad')
-rw-r--r--src/leap/soledad/__init__.py6
-rw-r--r--src/leap/soledad/server.py134
-rw-r--r--src/leap/soledad/tests/test_leap_backend.py8
-rw-r--r--src/leap/soledad/tests/test_server.py242
4 files changed, 388 insertions, 2 deletions
diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py
index 94a21447..c9bf3f76 100644
--- a/src/leap/soledad/__init__.py
+++ b/src/leap/soledad/__init__.py
@@ -71,6 +71,7 @@ Path to the certificate file used to certify the SSL connection between
Soledad client and server.
"""
+SECRETS_DOC_ID_HASH_PREFIX = 'uuid-'
#
# Exceptions
@@ -522,7 +523,10 @@ class Soledad(object):
@return: the hash
@rtype: str
"""
- return sha256('uuid-%s' % self._uuid).hexdigest()
+ return sha256(
+ '%s%s' % (
+ SECRETS_DOC_ID_HASH_PREFIX,
+ self._uuid)).hexdigest()
def _shared_db(self):
"""
diff --git a/src/leap/soledad/server.py b/src/leap/soledad/server.py
index e2944057..5a2bb81f 100644
--- a/src/leap/soledad/server.py
+++ b/src/leap/soledad/server.py
@@ -30,8 +30,13 @@ try:
except ImportError:
import json # noqa
+
+from hashlib import sha256
+from routes.mapper import Mapper
+from u1db import DBNAME_CONSTRAINTS
from u1db.remote import http_app
+
# Keep OpenSSL's tsafe before importing Twisted submodules so we can put
# it back if Twisted==12.0.0 messes with it.
from OpenSSL import tsafe
@@ -50,6 +55,7 @@ if version.base() == "12.0.0":
from couchdb.client import Server
+from leap.soledad import SECRETS_DOC_ID_HASH_PREFIX
from leap.soledad.backends.couch import CouchServerState
@@ -63,6 +69,115 @@ class Unauthorized(Exception):
"""
+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 sha256('%s%s' % (SECRETS_DOC_ID_HASH_PREFIX, uuid)).hexdigest()
+
+ 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.
@@ -150,6 +265,9 @@ class SoledadAuthMiddleware(object):
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)
@@ -183,6 +301,22 @@ class SoledadAuthMiddleware(object):
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
diff --git a/src/leap/soledad/tests/test_leap_backend.py b/src/leap/soledad/tests/test_leap_backend.py
index 2e4b3b01..458fc5d5 100644
--- a/src/leap/soledad/tests/test_leap_backend.py
+++ b/src/leap/soledad/tests/test_leap_backend.py
@@ -35,12 +35,13 @@ from u1db.remote import (
http_database,
http_target,
)
+from routes.mapper import Mapper
from leap import soledad
from leap.soledad.backends import leap_backend
from leap.soledad.server import (
SoledadApp,
- SoledadAuthMiddleware
+ SoledadAuthMiddleware,
)
from leap.soledad import auth
@@ -78,8 +79,13 @@ def make_token_soledad_app(state):
return True
return False
+ # we test for action authorization in leap.soledad.tests.test_server
+ def verify_action(environ, uuid):
+ return True
+
application = SoledadAuthMiddleware(app)
application.verify_token = verify_token
+ application.verify_action = verify_action
return application
diff --git a/src/leap/soledad/tests/test_server.py b/src/leap/soledad/tests/test_server.py
new file mode 100644
index 00000000..d574b50e
--- /dev/null
+++ b/src/leap/soledad/tests/test_server.py
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+# test_server.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/>.
+
+
+"""
+Tests for server-related functionality.
+"""
+
+import os
+import shutil
+import tempfile
+try:
+ import simplejson as json
+except ImportError:
+ import json # noqa
+import hashlib
+
+
+from leap.soledad.server import URLToAuth
+from leap.common.testing.basetest import BaseLeapTest
+
+
+class SoledadServerTestCase(BaseLeapTest):
+ """
+ Tests that guarantee that data will always be encrypted when syncing.
+ """
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def _make_environ(self, path_info, request_method):
+ return {
+ 'PATH_INFO': path_info,
+ 'REQUEST_METHOD': request_method,
+ }
+
+ def test_verify_action_with_correct_dbnames(self):
+ """
+ Test encrypting and decrypting documents.
+
+ The following table lists the authorized actions among all possible
+ u1db remote actions:
+
+ 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
+ """
+ uuid = 'myuuid'
+ authmap = URLToAuth(uuid)
+ dbname = authmap._uuid_dbname(uuid)
+ # test global auth
+ self.assertTrue(
+ authmap.is_authorized(self._make_environ('/', 'GET')))
+ # test shared-db database resource auth
+ self.assertTrue(
+ authmap.is_authorized(
+ self._make_environ('/shared', 'GET')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/shared', 'PUT')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/shared', 'DELETE')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/shared', 'POST')))
+ # test shared-db docs resource auth
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/shared/docs', 'GET')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/shared/docs', 'PUT')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/shared/docs', 'DELETE')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/shared/docs', 'POST')))
+ # test shared-db doc resource auth
+ self.assertTrue(
+ authmap.is_authorized(
+ self._make_environ('/shared/doc/x', 'GET')))
+ self.assertTrue(
+ authmap.is_authorized(
+ self._make_environ('/shared/doc/x', 'PUT')))
+ self.assertTrue(
+ authmap.is_authorized(
+ self._make_environ('/shared/doc/x', 'DELETE')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/shared/doc/x', 'POST')))
+ # test shared-db sync resource auth
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/shared/sync-from/x', 'GET')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/shared/sync-from/x', 'PUT')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/shared/sync-from/x', 'DELETE')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/shared/sync-from/x', 'POST')))
+ # test user-db database resource auth
+ self.assertTrue(
+ authmap.is_authorized(
+ self._make_environ('/%s' % dbname, 'GET')))
+ self.assertTrue(
+ authmap.is_authorized(
+ self._make_environ('/%s' % dbname, 'PUT')))
+ self.assertTrue(
+ authmap.is_authorized(
+ self._make_environ('/%s' % dbname, 'DELETE')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s' % dbname, 'POST')))
+ # test user-db docs resource auth
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/docs' % dbname, 'GET')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/docs' % dbname, 'PUT')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/docs' % dbname, 'DELETE')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/docs' % dbname, 'POST')))
+ # test user-db doc resource auth
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/doc/x' % dbname, 'GET')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/doc/x' % dbname, 'PUT')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/doc/x' % dbname, 'DELETE')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/doc/x' % dbname, 'POST')))
+ # test user-db sync resource auth
+ self.assertTrue(
+ authmap.is_authorized(
+ self._make_environ('/%s/sync-from/x' % dbname, 'GET')))
+ self.assertTrue(
+ authmap.is_authorized(
+ self._make_environ('/%s/sync-from/x' % dbname, 'PUT')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/sync-from/x' % dbname, 'DELETE')))
+ self.assertTrue(
+ authmap.is_authorized(
+ self._make_environ('/%s/sync-from/x' % dbname, 'POST')))
+
+ def test_verify_action_with_wrong_dbnames(self):
+ """
+ Test if authorization fails for a wrong dbname.
+ """
+ uuid = 'myuuid'
+ authmap = URLToAuth(uuid)
+ dbname = 'somedb'
+ # test wrong-db database resource auth
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s' % dbname, 'GET')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s' % dbname, 'PUT')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s' % dbname, 'DELETE')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s' % dbname, 'POST')))
+ # test wrong-db docs resource auth
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/docs' % dbname, 'GET')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/docs' % dbname, 'PUT')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/docs' % dbname, 'DELETE')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/docs' % dbname, 'POST')))
+ # test wrong-db doc resource auth
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/doc/x' % dbname, 'GET')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/doc/x' % dbname, 'PUT')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/doc/x' % dbname, 'DELETE')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/doc/x' % dbname, 'POST')))
+ # test wrong-db sync resource auth
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/sync-from/x' % dbname, 'GET')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/sync-from/x' % dbname, 'PUT')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/sync-from/x' % dbname, 'DELETE')))
+ self.assertFalse(
+ authmap.is_authorized(
+ self._make_environ('/%s/sync-from/x' % dbname, 'POST')))