summaryrefslogtreecommitdiff
path: root/src/leap/soledad/server.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/soledad/server.py')
-rw-r--r--src/leap/soledad/server.py162
1 files changed, 148 insertions, 14 deletions
diff --git a/src/leap/soledad/server.py b/src/leap/soledad/server.py
index 7aa253a3..9c9e0ad7 100644
--- a/src/leap/soledad/server.py
+++ b/src/leap/soledad/server.py
@@ -25,19 +25,34 @@ This should be run with:
import configparser
import httplib
-try:
- import simplejson as json
-except ImportError:
- import json # noqa
+import simplejson as json
+
+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
+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
+ # come to use Twisted>=12.3.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.backends.couch import CouchServerState
@@ -51,6 +66,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.
@@ -138,6 +262,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)
@@ -171,20 +298,21 @@ class SoledadAuthMiddleware(object):
return False
return True
- def need_auth(self, environ):
+ def verify_action(self, uuid, environ):
"""
- Check if action can be performed on database without authentication.
-
- For now, just allow access to /shared/*.
+ 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 requests needs authentication.
+ @return: Whether the user is authorize to perform the requested action
+ over the requested db.
@rtype: bool
"""
- # TODO: design unauth verification.
- return not environ.get(self.PATH_INFO_KEY).startswith('/shared/')
+ return URLToAuth(uuid).is_authorized(environ)
#-----------------------------------------------------------------------------
@@ -196,6 +324,11 @@ class SoledadApp(http_app.HTTPApp):
Soledad WSGI application
"""
+ SHARED_DB_NAME = 'shared'
+ """
+ The name of the shared database that holds user's encrypted secrets.
+ """
+
def __call__(self, environ, start_response):
"""
Handle a WSGI call to the Soledad application.
@@ -209,6 +342,8 @@ class SoledadApp(http_app.HTTPApp):
@return: HTTP application results.
@rtype: list
"""
+ # ensure the shared database exists
+ self.state.ensure_database(self.SHARED_DB_NAME)
return http_app.HTTPApp.__call__(self, environ, start_response)
@@ -244,11 +379,10 @@ def load_configuration(file_path):
# Run as Twisted WSGI Resource
#-----------------------------------------------------------------------------
-# TODO: create command-line option for choosing config file.
conf = load_configuration('/etc/leap/soledad-server.conf')
state = CouchServerState(conf['couch_url'])
-application = SoledadAuthMiddleware(
- SoledadApp(state))
+# WSGI application that may be used by `twistd -web`
+application = SoledadAuthMiddleware(SoledadApp(state))
resource = WSGIResource(reactor, reactor.getThreadPool(), application)