diff options
| author | drebs <drebs@leap.se> | 2013-05-24 12:59:21 -0300 | 
|---|---|---|
| committer | drebs <drebs@leap.se> | 2013-05-25 11:20:13 -0300 | 
| commit | d022428a695ba5c499fcd0c8a9681962c006b8e8 (patch) | |
| tree | 5d0d3a525f81a39f2982c114b5f048ee2af322d3 /src | |
| parent | 8cef16cd458801b1513bbcd6849edc4599204b6f (diff) | |
Add action validation in server.
* Use routes for validating user actions when interacting with server.
* Also add tests for action validation.
* Add changes file.
* Closes #2356.
Diffstat (limited to 'src')
| -rw-r--r-- | src/leap/soledad/__init__.py | 6 | ||||
| -rw-r--r-- | src/leap/soledad/server.py | 134 | ||||
| -rw-r--r-- | src/leap/soledad/tests/test_leap_backend.py | 8 | ||||
| -rw-r--r-- | src/leap/soledad/tests/test_server.py | 242 | 
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'))) | 
