diff options
| -rw-r--r-- | README.rst | 16 | ||||
| -rw-r--r-- | soledad/src/leap/soledad/auth.py | 3 | ||||
| -rw-r--r-- | soledad/src/leap/soledad/tests/test_server.py | 17 | ||||
| -rw-r--r-- | soledad/src/leap/soledad/tests/test_target.py | 20 | ||||
| -rw-r--r-- | soledad_server/changes/2621-refactor-audh-to-make-it-adaptable | 2 | ||||
| -rw-r--r-- | soledad_server/src/leap/soledad_server/__init__.py | 274 | ||||
| -rw-r--r-- | soledad_server/src/leap/soledad_server/auth.py | 431 | 
7 files changed, 466 insertions, 297 deletions
@@ -10,7 +10,19 @@ This software is under development.  Tests  ----- -To run CouchDB tests, be sure you have ``CouchDB`` installed on your system. -Tests can be run with:: +Client and server tests are both included in leap.soledad. Because +soledad_server depends on soledad and soledad tests depend on soledad_server, +if you want to run tests in development mode you must first install soledad, +then soledad_server, and then run the tests. +Therefore, tests must be run with:: + +  cd soledad +  python setup.py develop +  cd ../soledad_server +  python setup.py develop +  cd ../soledad    python setup.py test + +Note that to run CouchDB tests, be sure you have ``CouchDB`` installed on your +system. 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/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/changes/2621-refactor-audh-to-make-it-adaptable b/soledad_server/changes/2621-refactor-audh-to-make-it-adaptable new file mode 100644 index 00000000..775f7e8d --- /dev/null +++ b/soledad_server/changes/2621-refactor-audh-to-make-it-adaptable @@ -0,0 +1,2 @@ +  o Refactor server side auth classes to make it possible for other kinds of +    authentication to be easily implemented. Closes #2621. diff --git a/soledad_server/src/leap/soledad_server/__init__.py b/soledad_server/src/leap/soledad_server/__init__.py index bea5d5fd..afbb057e 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  #----------------------------------------------------------------------------- @@ -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) +resource = WSGIResource(reactor, reactor.getThreadPool(), application)
\ No newline at end of file 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  | 
