From a39af0e003ba95c9b7ab554aa4a4c5ce316a43c7 Mon Sep 17 00:00:00 2001 From: drebs Date: Sun, 18 Dec 2016 12:56:21 -0200 Subject: [bug] disallow all requests to "user-{uuid}/" --- server/src/leap/soledad/server/auth.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index b0764569..f3d9c8a8 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -100,7 +100,7 @@ class URLToAuthorization(object): /shared-db/docs | - /shared-db/doc/{any_id} | GET, PUT, DELETE /shared-db/sync-from/{source} | - - /user-db | GET, PUT, DELETE + /user-db | - /user-db/docs | - /user-db/doc/{id} | - /user-db/sync-from/{source} | GET, PUT, POST @@ -108,19 +108,12 @@ class URLToAuthorization(object): # auth info for global resource self._register('/', [self.HTTP_METHOD_GET]) # auth info for shared-db database resource - self._register( - '/%s' % SHARED_DB_NAME, - [self.HTTP_METHOD_GET]) + self._register('/%s' % SHARED_DB_NAME, [self.HTTP_METHOD_GET]) # auth info for shared-db doc resource self._register( '/%s/doc/{id:.*}' % 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' % self._user_db_name, - [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}' % self._user_db_name, -- cgit v1.2.3 From e73d36621052a69aae327200c063ac1689bcf9e0 Mon Sep 17 00:00:00 2001 From: drebs Date: Sun, 18 Dec 2016 14:21:54 -0200 Subject: [feat] reuse the url mapper instead of creating it for every request --- server/src/leap/soledad/server/auth.py | 82 +++++++++------------------------- 1 file changed, 22 insertions(+), 60 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index f3d9c8a8..c026a282 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -26,67 +26,31 @@ from routes.mapper import Mapper from leap.soledad.common.log import getLogger from leap.soledad.common.l2db import DBNAME_CONSTRAINTS, errors as u1db_errors from leap.soledad.common import SHARED_DB_NAME -from leap.soledad.common import USER_DB_PREFIX logger = getLogger(__name__) -class URLToAuthorization(object): +class URLMapper(object): """ - Verify if actions can be performed by a user. + Maps the URLs users can access. """ - 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 - @param user_db_prefix: The string prefix of users' databases. - @type user_db_prefix: str - """ + def __init__(self): self._map = Mapper(controller_scan=None) - self._user_db_name = "%s%s" % (USER_DB_PREFIX, uuid) - self._uuid = uuid - self._register_auth_info() - - 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 + self._connect_urls() + self._map.create_regs() - def _register(self, pattern, http_methods): - """ - Register a C{pattern} in the mapper as valid for C{http_methods}. + def match(self, environ): + return self._map.match(environ=environ) - @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 - """ + def _connect(self, pattern, http_methods): self._map.connect( None, pattern, http_methods=http_methods, conditions=dict(method=http_methods), requirements={'dbname': DBNAME_CONSTRAINTS}) - def _register_auth_info(self): + def _connect_urls(self): """ Register the authorization info in the mapper using C{SHARED_DB_NAME} as the user's database name. @@ -106,21 +70,15 @@ class URLToAuthorization(object): /user-db/sync-from/{source} | GET, PUT, POST """ # auth info for global resource - self._register('/', [self.HTTP_METHOD_GET]) + self._connect('/', ['GET']) # auth info for shared-db database resource - self._register('/%s' % SHARED_DB_NAME, [self.HTTP_METHOD_GET]) + self._connect('/%s' % SHARED_DB_NAME, ['GET']) # auth info for shared-db doc resource - self._register( - '/%s/doc/{id:.*}' % SHARED_DB_NAME, - [self.HTTP_METHOD_GET, self.HTTP_METHOD_PUT, - self.HTTP_METHOD_DELETE]) + self._connect('/%s/doc/{id:.*}' % SHARED_DB_NAME, + ['GET', 'PUT', 'DELETE']) # auth info for user-db sync resource - self._register( - '/%s/sync-from/{source_replica_uid}' % self._user_db_name, - [self.HTTP_METHOD_GET, self.HTTP_METHOD_PUT, - self.HTTP_METHOD_POST]) - # generate the regular expressions - self._map.create_regs() + self._connect('/user-{uuid}/sync-from/{source_replica_uid}', + ['GET', 'PUT', 'POST']) class SoledadAuthMiddleware(object): @@ -176,6 +134,7 @@ class SoledadAuthMiddleware(object): @type prefix: str """ self._app = app + self._mapper = URLMapper() def _error(self, start_response, status, description, message=None): """ @@ -310,14 +269,17 @@ class SoledadAuthMiddleware(object): @param environ: Dictionary containing CGI variables. @type environ: dict - @param uuid: The user's uuid. + @param uuid: The user's uuid from the Authorization header. @type uuid: str - @return: Whether the user is authorize to perform the requested action + @return: Whether the user is authorized to perform the requested action over the requested db. @rtype: bool """ - return URLToAuthorization(uuid).is_authorized(environ) + match = self._mapper.match(environ) + if not match: + return False + return uuid == match.get('uuid') @abstractmethod def _get_auth_error_string(self): -- cgit v1.2.3 From 260805b9967184841c4499f94713a9a48c49a813 Mon Sep 17 00:00:00 2001 From: drebs Date: Sun, 18 Dec 2016 16:36:39 -0200 Subject: [feat] use twisted web http auth and creds --- server/src/leap/soledad/server/application.py | 16 +- server/src/leap/soledad/server/auth.py | 384 ++++++++------------------ server/src/leap/soledad/server/resource.py | 4 +- server/src/leap/soledad/server/session.py | 78 ++++++ 4 files changed, 199 insertions(+), 283 deletions(-) create mode 100644 server/src/leap/soledad/server/session.py (limited to 'server/src') diff --git a/server/src/leap/soledad/server/application.py b/server/src/leap/soledad/server/application.py index 17296425..8fd343b3 100644 --- a/server/src/leap/soledad/server/application.py +++ b/server/src/leap/soledad/server/application.py @@ -24,7 +24,6 @@ Use it like this: from twisted.internet import reactor from leap.soledad.server import SoledadApp -from leap.soledad.server.auth import SoledadTokenAuthMiddleware from leap.soledad.server.gzip_middleware import GzipMiddleware from leap.soledad.server.config import load_configuration from leap.soledad.common.backend import SoledadBackend @@ -35,20 +34,25 @@ from leap.soledad.common.log import getLogger __all__ = ['wsgi_application'] -def _load_config(): - conf = load_configuration('/etc/soledad/soledad-server.conf') - return conf['soledad-server'] +_config = None + + +def get_config(): + global _config + if not _config: + _config = load_configuration('/etc/soledad/soledad-server.conf') + return _config['soledad-server'] def _get_couch_state(): - conf = _load_config() + conf = get_config() state = CouchServerState(conf['couch_url'], create_cmd=conf['create_cmd'], check_schema_versions=True) SoledadBackend.BATCH_SUPPORT = conf.get('batching', False) return state -_app = SoledadTokenAuthMiddleware(SoledadApp(None)) # delay state init +_app = SoledadApp(None) # delay state init wsgi_application = GzipMiddleware(_app) diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index c026a282..f55b710e 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -15,20 +15,110 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ -Authentication facilities for Soledad Server. +Twisted http token auth. """ -import httplib -import json +import binascii +import time -from abc import ABCMeta, abstractmethod +from hashlib import sha512 from routes.mapper import Mapper +from zope.interface import implementer + +from twisted.cred import error +from twisted.cred.checkers import ICredentialsChecker +from twisted.cred.credentials import IUsernamePassword +from twisted.cred.credentials import UsernamePassword +from twisted.cred.portal import IRealm +from twisted.cred.portal import Portal +from twisted.web.iweb import ICredentialFactory +from twisted.web.resource import IResource -from leap.soledad.common.log import getLogger -from leap.soledad.common.l2db import DBNAME_CONSTRAINTS, errors as u1db_errors from leap.soledad.common import SHARED_DB_NAME +from leap.soledad.common.couch import couch_server +from leap.soledad.common.l2db import DBNAME_CONSTRAINTS +from leap.soledad.server.resource import SoledadResource +from leap.soledad.server.application import get_config + + +@implementer(IRealm) +class SoledadRealm(object): + + def requestAvatar(self, avatarId, mind, *interfaces): + if IResource in interfaces: + return (IResource, SoledadResource(avatarId), lambda: None) + raise NotImplementedError() + + +@implementer(ICredentialsChecker) +class TokenChecker(object): + + credentialInterfaces = [IUsernamePassword] + + TOKENS_DB_PREFIX = "tokens_" + TOKENS_DB_EXPIRE = 30 * 24 * 3600 # 30 days in seconds + TOKENS_TYPE_KEY = "type" + TOKENS_TYPE_DEF = "Token" + TOKENS_USER_ID_KEY = "user_id" + + def __init__(self): + config = get_config() + self.couch_url = config['couch_url'] + + def _tokens_dbname(self): + dbname = self.TOKENS_DB_PREFIX + \ + str(int(time.time() / self.TOKENS_DB_EXPIRE)) + return dbname + + def requestAvatarId(self, credentials): + uuid = credentials.username + token = credentials.password + with couch_server(self.couch_url) as server: + # the tokens db rotates every 30 days, and the current db name is + # "tokens_NNN", where NNN is the number of seconds since epoch + # divide dby the rotate period in seconds. When rotating, old and + # new tokens db coexist during a certain window of time and valid + # tokens are replicated from the old db to the new one. See: + # https://leap.se/code/issues/6785 + dbname = self._tokens_dbname() + db = server[dbname] + # lookup key is a hash of the token to prevent timing attacks. + token = db.get(sha512(token).hexdigest()) + if token is None: + return False + # we compare uuid hashes to avoid possible timing attacks that + # might exploit python's builtin comparison operator behaviour, + # which fails immediatelly when non-matching bytes are found. + couch_uuid_hash = sha512(token[self.TOKENS_USER_ID_KEY]).digest() + req_uuid_hash = sha512(uuid).digest() + if token[self.TOKENS_TYPE_KEY] != self.TOKENS_TYPE_DEF \ + or couch_uuid_hash != req_uuid_hash: + return False + return True -logger = getLogger(__name__) +@implementer(ICredentialFactory) +class TokenCredentialFactory(object): + + scheme = 'token' + + def getChallenge(self, request): + return {} + + def decode(self, response, request): + try: + creds = response.decode('base64') + except binascii.Error: + raise error.LoginFailed('Invalid credentials') + + creds = creds.split(b':', 1) + if len(creds) == 2: + return UsernamePassword(*creds) + else: + raise error.LoginFailed('Invalid credentials') + + +portal = Portal(SoledadRealm(), [TokenChecker()]) +credentialFactory = TokenCredentialFactory() class URLMapper(object): @@ -41,7 +131,8 @@ class URLMapper(object): self._connect_urls() self._map.create_regs() - def match(self, environ): + def match(self, path, method): + environ = {'PATH_INFO': path, 'REQUEST_METHOD': method} return self._map.match(environ=environ) def _connect(self, pattern, http_methods): @@ -81,272 +172,15 @@ class URLMapper(object): ['GET', 'PUT', 'POST']) -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 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(":") - - 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__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 - self._mapper = URLMapper() - - 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 +@implementer(IResource) +class UnauthorizedResource(object): + isLeaf = True - @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( - start_response, "Wrong authentication scheme") - - # verify if user is athenticated - try: - if not self._verify_authentication_data(uuid, auth_data): - return self._unauthorized_error( - start_response, - self._get_auth_error_string()) - except u1db_errors.Unauthorized as e: - return self._error( - start_response, - 401, - e.wire_description) - - # 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 - - @raise Unauthorized: Raised when C{auth_data} is not enough to - authenticate C{uuid}. - """ - return None + def render(self, request): + request.setResponseCode(401) + if request.method == b'HEAD': + return b'' + return b'Unauthorized' - 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 from the Authorization header. - @type uuid: str - - @return: Whether the user is authorized to perform the requested action - over the requested db. - @rtype: bool - """ - match = self._mapper.match(environ) - if not match: - return False - return uuid == match.get('uuid') - - @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. - """ - - TOKEN_AUTH_ERROR_STRING = "Incorrect address or token." - - def _get_state(self): - return self._app.state - - def _set_state(self, state): - self._app.state = state - - state = property(_get_state, _set_state) - - 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 - - @raise Unauthorized: Raised when C{auth_data} is not enough to - authenticate C{uuid}. - """ - token = auth_data # we expect a cleartext token at this point - try: - return self.state.verify_token(uuid, token) - except Exception as e: - logger.error(e) - return False - - def _get_auth_error_string(self): - """ - Get the error string for token auth. - - @return: The error string. - """ - return self.TOKEN_AUTH_ERROR_STRING + def getChildWithDefault(self, path, request): + return self diff --git a/server/src/leap/soledad/server/resource.py b/server/src/leap/soledad/server/resource.py index dbb91b0a..9922c997 100644 --- a/server/src/leap/soledad/server/resource.py +++ b/server/src/leap/soledad/server/resource.py @@ -17,7 +17,6 @@ """ A twisted resource that serves the Soledad Server. """ - from twisted.web.resource import Resource from twisted.web.wsgi import WSGIResource from twisted.internet import reactor @@ -42,7 +41,8 @@ class SoledadResource(Resource): for the Soledad Server. """ - def __init__(self): + def __init__(self, uuid): + self._uuid = uuid self.children = {'': wsgi_resource} def getChild(self, path, request): diff --git a/server/src/leap/soledad/server/session.py b/server/src/leap/soledad/server/session.py new file mode 100644 index 00000000..22e1d1fb --- /dev/null +++ b/server/src/leap/soledad/server/session.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# session.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 . +""" +Twisted resource containing an authenticated Soledad session. +""" +from zope.interface import implementer + +from twisted.cred import error +from twisted.python import log +from twisted.web import util +from twisted.web.guard import HTTPAuthSessionWrapper +from twisted.web.resource import ErrorPage +from twisted.web.resource import IResource + +from leap.soledad.server.auth import URLMapper +from leap.soledad.server.auth import portal +from leap.soledad.server.auth import credentialFactory +from leap.soledad.server.auth import UnauthorizedResource + + +@implementer(IResource) +class SoledadSession(HTTPAuthSessionWrapper): + + def __init__(self): + self._mapper = URLMapper() + self._portal = portal + self._credentialFactory = credentialFactory + + def _matchPath(self, request): + match = self._mapper.match(request.path, request.method) + return match + + def _parseHeader(self, header): + elements = header.split(b' ') + scheme = elements[0].lower() + if scheme == self._credentialFactory.scheme: + return (b' '.join(elements[1:])) + return None + + def _authorizedResource(self, request): + match = self._matchPath(request) + if not match: + return UnauthorizedResource() + + header = request.getHeader(b'authorization') + if not header: + return UnauthorizedResource() + + auth_data = self._parseHeader(header) + if not auth_data: + return UnauthorizedResource() + + try: + credentials = self._credentialFactory.decode(auth_data, request) + except error.LoginFailed: + return UnauthorizedResource() + except: + log.err(None, "Unexpected failure from credentials factory") + return ErrorPage(500, None, None) + else: + request_uuid = match.get('uuid') + if request_uuid and request_uuid != credentials.username: + return ErrorPage(500, None, None) + return util.DeferredResource(self._login(credentials)) -- cgit v1.2.3 From c39bde684da223c46368605368f63ac1beb8b6d4 Mon Sep 17 00:00:00 2001 From: drebs Date: Sun, 18 Dec 2016 21:06:26 -0200 Subject: [feat] cache session data in server --- server/src/leap/soledad/server/auth.py | 2 +- server/src/leap/soledad/server/resource.py | 3 +- server/src/leap/soledad/server/session.py | 55 +++++++++++++++++++++++++++--- 3 files changed, 52 insertions(+), 8 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index f55b710e..c5b90359 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -45,7 +45,7 @@ class SoledadRealm(object): def requestAvatar(self, avatarId, mind, *interfaces): if IResource in interfaces: - return (IResource, SoledadResource(avatarId), lambda: None) + return (IResource, SoledadResource(), lambda: None) raise NotImplementedError() diff --git a/server/src/leap/soledad/server/resource.py b/server/src/leap/soledad/server/resource.py index 9922c997..67e9ae32 100644 --- a/server/src/leap/soledad/server/resource.py +++ b/server/src/leap/soledad/server/resource.py @@ -41,8 +41,7 @@ class SoledadResource(Resource): for the Soledad Server. """ - def __init__(self, uuid): - self._uuid = uuid + def __init__(self): self.children = {'': wsgi_resource} def getChild(self, path, request): diff --git a/server/src/leap/soledad/server/session.py b/server/src/leap/soledad/server/session.py index 22e1d1fb..75440089 100644 --- a/server/src/leap/soledad/server/session.py +++ b/server/src/leap/soledad/server/session.py @@ -21,15 +21,41 @@ from zope.interface import implementer from twisted.cred import error from twisted.python import log +from twisted.python.components import registerAdapter from twisted.web import util from twisted.web.guard import HTTPAuthSessionWrapper from twisted.web.resource import ErrorPage from twisted.web.resource import IResource +from twisted.web.server import Session +from zope.interface import Interface +from zope.interface import Attribute from leap.soledad.server.auth import URLMapper from leap.soledad.server.auth import portal from leap.soledad.server.auth import credentialFactory from leap.soledad.server.auth import UnauthorizedResource +from leap.soledad.server.resource import SoledadResource + + +class ISessionData(Interface): + username = Attribute('An uuid.') + password = Attribute('A token.') + + +@implementer(ISessionData) +class SessionData(object): + def __init__(self, session): + self.username = None + self.password = None + + +registerAdapter(SessionData, Session, ISessionData) + + +def _sessionData(request): + session = request.getSession() + data = ISessionData(session) + return data @implementer(IResource) @@ -71,8 +97,27 @@ class SoledadSession(HTTPAuthSessionWrapper): except: log.err(None, "Unexpected failure from credentials factory") return ErrorPage(500, None, None) - else: - request_uuid = match.get('uuid') - if request_uuid and request_uuid != credentials.username: - return ErrorPage(500, None, None) - return util.DeferredResource(self._login(credentials)) + + request_uuid = match.get('uuid') + if request_uuid and request_uuid != credentials.username: + return ErrorPage(500, None, None) + + # eventually return a cached resouce + sessionData = _sessionData(request) + if sessionData.username == credentials.username \ + and sessionData.password == credentials.password: + return SoledadResource() + + return util.DeferredResource(self._login(credentials, sessionData)) + + def _login(self, credentials, sessionData): + + def _cacheSessionData(res): + sessionData.username = credentials.username + sessionData.password = credentials.password + return res + + d = self._portal.login(credentials, None, IResource) + d.addCallback(_cacheSessionData) + d.addCallbacks(self._loginSucceeded, self._loginFailed) + return d -- cgit v1.2.3 From 6043f7966b64d6922987bca9137a524fb06a3379 Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 19 Dec 2016 09:43:03 -0200 Subject: [refactor] separate url mapper, avoid hanging tests Because the wsgi resource has its own threadpool, tests might get confused when shutting down and the reactor may get clogged waiting for the threadpool to be stopped. By refactoring the URLMapper to its own module, server tests can avoid loading the resource module, where the wsgi threadpool resides, so the threapool will not be started. --- server/src/leap/soledad/server/auth.py | 68 ------------------------- server/src/leap/soledad/server/session.py | 17 ++++++- server/src/leap/soledad/server/url_mapper.py | 74 ++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 70 deletions(-) create mode 100644 server/src/leap/soledad/server/url_mapper.py (limited to 'server/src') diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index c5b90359..13245cfe 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -21,7 +21,6 @@ import binascii import time from hashlib import sha512 -from routes.mapper import Mapper from zope.interface import implementer from twisted.cred import error @@ -33,9 +32,7 @@ from twisted.cred.portal import Portal from twisted.web.iweb import ICredentialFactory from twisted.web.resource import IResource -from leap.soledad.common import SHARED_DB_NAME from leap.soledad.common.couch import couch_server -from leap.soledad.common.l2db import DBNAME_CONSTRAINTS from leap.soledad.server.resource import SoledadResource from leap.soledad.server.application import get_config @@ -119,68 +116,3 @@ class TokenCredentialFactory(object): portal = Portal(SoledadRealm(), [TokenChecker()]) credentialFactory = TokenCredentialFactory() - - -class URLMapper(object): - """ - Maps the URLs users can access. - """ - - def __init__(self): - self._map = Mapper(controller_scan=None) - self._connect_urls() - self._map.create_regs() - - def match(self, path, method): - environ = {'PATH_INFO': path, 'REQUEST_METHOD': method} - return self._map.match(environ=environ) - - def _connect(self, pattern, http_methods): - self._map.connect( - None, pattern, http_methods=http_methods, - conditions=dict(method=http_methods), - requirements={'dbname': DBNAME_CONSTRAINTS}) - - def _connect_urls(self): - """ - Register the authorization info in the mapper using C{SHARED_DB_NAME} - 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/{any_id} | GET, PUT, DELETE - /shared-db/sync-from/{source} | - - /user-db | - - /user-db/docs | - - /user-db/doc/{id} | - - /user-db/sync-from/{source} | GET, PUT, POST - """ - # auth info for global resource - self._connect('/', ['GET']) - # auth info for shared-db database resource - self._connect('/%s' % SHARED_DB_NAME, ['GET']) - # auth info for shared-db doc resource - self._connect('/%s/doc/{id:.*}' % SHARED_DB_NAME, - ['GET', 'PUT', 'DELETE']) - # auth info for user-db sync resource - self._connect('/user-{uuid}/sync-from/{source_replica_uid}', - ['GET', 'PUT', 'POST']) - - -@implementer(IResource) -class UnauthorizedResource(object): - isLeaf = True - - def render(self, request): - request.setResponseCode(401) - if request.method == b'HEAD': - return b'' - return b'Unauthorized' - - def getChildWithDefault(self, path, request): - return self diff --git a/server/src/leap/soledad/server/session.py b/server/src/leap/soledad/server/session.py index 75440089..1ef5b6a6 100644 --- a/server/src/leap/soledad/server/session.py +++ b/server/src/leap/soledad/server/session.py @@ -30,10 +30,9 @@ from twisted.web.server import Session from zope.interface import Interface from zope.interface import Attribute -from leap.soledad.server.auth import URLMapper from leap.soledad.server.auth import portal from leap.soledad.server.auth import credentialFactory -from leap.soledad.server.auth import UnauthorizedResource +from leap.soledad.server.urlmapper import URLMapper from leap.soledad.server.resource import SoledadResource @@ -58,6 +57,20 @@ def _sessionData(request): return data +@implementer(IResource) +class UnauthorizedResource(object): + isLeaf = True + + def render(self, request): + request.setResponseCode(401) + if request.method == b'HEAD': + return b'' + return b'Unauthorized' + + def getChildWithDefault(self, path, request): + return self + + @implementer(IResource) class SoledadSession(HTTPAuthSessionWrapper): diff --git a/server/src/leap/soledad/server/url_mapper.py b/server/src/leap/soledad/server/url_mapper.py new file mode 100644 index 00000000..483f7e87 --- /dev/null +++ b/server/src/leap/soledad/server/url_mapper.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# url_mapper.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 . +""" +An URL mapper that represents authorized paths. +""" +from routes.mapper import Mapper + +from leap.soledad.common import SHARED_DB_NAME +from leap.soledad.common.l2db import DBNAME_CONSTRAINTS + + +class URLMapper(object): + """ + Maps the URLs users can access. + """ + + def __init__(self): + self._map = Mapper(controller_scan=None) + self._connect_urls() + self._map.create_regs() + + def match(self, path, method): + environ = {'PATH_INFO': path, 'REQUEST_METHOD': method} + return self._map.match(environ=environ) + + def _connect(self, pattern, http_methods): + self._map.connect( + None, pattern, http_methods=http_methods, + conditions=dict(method=http_methods), + requirements={'dbname': DBNAME_CONSTRAINTS}) + + def _connect_urls(self): + """ + Register the authorization info in the mapper using C{SHARED_DB_NAME} + 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/{any_id} | GET, PUT, DELETE + /shared-db/sync-from/{source} | - + /user-db | - + /user-db/docs | - + /user-db/doc/{id} | - + /user-db/sync-from/{source} | GET, PUT, POST + """ + # auth info for global resource + self._connect('/', ['GET']) + # auth info for shared-db database resource + self._connect('/%s' % SHARED_DB_NAME, ['GET']) + # auth info for shared-db doc resource + self._connect('/%s/doc/{id:.*}' % SHARED_DB_NAME, + ['GET', 'PUT', 'DELETE']) + # auth info for user-db sync resource + self._connect('/user-{uuid}/sync-from/{source_replica_uid}', + ['GET', 'PUT', 'POST']) -- cgit v1.2.3 From 9afcf45dc0e12073eaec7688061f03cc7e1dbd40 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 22 Dec 2016 07:33:11 -0200 Subject: [bug] fix name of module on import --- server/src/leap/soledad/server/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/session.py b/server/src/leap/soledad/server/session.py index 1ef5b6a6..59424a7b 100644 --- a/server/src/leap/soledad/server/session.py +++ b/server/src/leap/soledad/server/session.py @@ -32,7 +32,7 @@ from zope.interface import Attribute from leap.soledad.server.auth import portal from leap.soledad.server.auth import credentialFactory -from leap.soledad.server.urlmapper import URLMapper +from leap.soledad.server.url_mapper import URLMapper from leap.soledad.server.resource import SoledadResource -- cgit v1.2.3 From 4fce575de20effc9c4d934028f8ccdfbd97932e1 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 29 Dec 2016 09:28:10 -0200 Subject: [refactor] remove twisted session persistence The need for token caching in server is a matter of debate, as is the ideal way to do it. Twisted sessions store the session id in a cookie and use that session id to persist. It is not clear if that implementation is needed, works with future features (as multiple soledad servers) or represents a security problem in some way. Because of these, this commit removes it for now. The feature is left in git history so we can bring it back later if needed. --- server/src/leap/soledad/server/session.py | 45 ++----------------------------- 1 file changed, 2 insertions(+), 43 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/session.py b/server/src/leap/soledad/server/session.py index 59424a7b..4ed2721c 100644 --- a/server/src/leap/soledad/server/session.py +++ b/server/src/leap/soledad/server/session.py @@ -21,40 +21,14 @@ from zope.interface import implementer from twisted.cred import error from twisted.python import log -from twisted.python.components import registerAdapter from twisted.web import util from twisted.web.guard import HTTPAuthSessionWrapper from twisted.web.resource import ErrorPage from twisted.web.resource import IResource -from twisted.web.server import Session -from zope.interface import Interface -from zope.interface import Attribute from leap.soledad.server.auth import portal from leap.soledad.server.auth import credentialFactory from leap.soledad.server.url_mapper import URLMapper -from leap.soledad.server.resource import SoledadResource - - -class ISessionData(Interface): - username = Attribute('An uuid.') - password = Attribute('A token.') - - -@implementer(ISessionData) -class SessionData(object): - def __init__(self, session): - self.username = None - self.password = None - - -registerAdapter(SessionData, Session, ISessionData) - - -def _sessionData(request): - session = request.getSession() - data = ISessionData(session) - return data @implementer(IResource) @@ -115,22 +89,7 @@ class SoledadSession(HTTPAuthSessionWrapper): if request_uuid and request_uuid != credentials.username: return ErrorPage(500, None, None) - # eventually return a cached resouce - sessionData = _sessionData(request) - if sessionData.username == credentials.username \ - and sessionData.password == credentials.password: - return SoledadResource() - - return util.DeferredResource(self._login(credentials, sessionData)) - - def _login(self, credentials, sessionData): + return util.DeferredResource(self._login(credentials)) - def _cacheSessionData(res): - sessionData.username = credentials.username - sessionData.password = credentials.password - return res - d = self._portal.login(credentials, None, IResource) - d.addCallback(_cacheSessionData) - d.addCallbacks(self._loginSucceeded, self._loginFailed) - return d +soledadSession = SoledadSession() -- cgit v1.2.3 From b3e0af399b81b65d6665865d1e1b18aa589f5824 Mon Sep 17 00:00:00 2001 From: drebs Date: Tue, 31 Jan 2017 23:51:42 -0200 Subject: [test] add tests for server auth --- server/src/leap/soledad/server/auth.py | 50 ++++++++++++++++++++----------- server/src/leap/soledad/server/session.py | 4 +-- 2 files changed, 35 insertions(+), 19 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index 13245cfe..bcef2e7c 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -29,6 +29,7 @@ from twisted.cred.credentials import IUsernamePassword from twisted.cred.credentials import UsernamePassword from twisted.cred.portal import IRealm from twisted.cred.portal import Portal +from twisted.internet import defer from twisted.web.iweb import ICredentialFactory from twisted.web.resource import IResource @@ -57,31 +58,45 @@ class TokenChecker(object): TOKENS_TYPE_DEF = "Token" TOKENS_USER_ID_KEY = "user_id" - def __init__(self): - config = get_config() - self.couch_url = config['couch_url'] + def __init__(self, server=None): + if server is None: + config = get_config() + couch_url = config['couch_url'] + server = couch_server(couch_url) + self._server = server + self._dbs = {} def _tokens_dbname(self): dbname = self.TOKENS_DB_PREFIX + \ str(int(time.time() / self.TOKENS_DB_EXPIRE)) return dbname + def _get_db(self, dbname): + if dbname not in self._dbs: + self._dbs[dbname] = self._server[dbname] + return self._dbs[dbname] + + def _tokens_db(self): + # the tokens db rotates every 30 days, and the current db name is + # "tokens_NNN", where NNN is the number of seconds since epoch + # divide dby the rotate period in seconds. When rotating, old and + # new tokens db coexist during a certain window of time and valid + # tokens are replicated from the old db to the new one. See: + # https://leap.se/code/issues/6785 + dbname = self._tokens_dbname() + db = self._get_db(dbname) + return db + def requestAvatarId(self, credentials): uuid = credentials.username token = credentials.password - with couch_server(self.couch_url) as server: - # the tokens db rotates every 30 days, and the current db name is - # "tokens_NNN", where NNN is the number of seconds since epoch - # divide dby the rotate period in seconds. When rotating, old and - # new tokens db coexist during a certain window of time and valid - # tokens are replicated from the old db to the new one. See: - # https://leap.se/code/issues/6785 - dbname = self._tokens_dbname() - db = server[dbname] + # lookup key is a hash of the token to prevent timing attacks. + db = self._tokens_db() token = db.get(sha512(token).hexdigest()) if token is None: - return False + return defer.fail(error.UnauthorizedLogin()) + # we compare uuid hashes to avoid possible timing attacks that # might exploit python's builtin comparison operator behaviour, # which fails immediatelly when non-matching bytes are found. @@ -89,8 +104,9 @@ class TokenChecker(object): req_uuid_hash = sha512(uuid).digest() if token[self.TOKENS_TYPE_KEY] != self.TOKENS_TYPE_DEF \ or couch_uuid_hash != req_uuid_hash: - return False - return True + return defer.fail(error.UnauthorizedLogin()) + + return defer.succeed(uuid) @implementer(ICredentialFactory) @@ -103,7 +119,7 @@ class TokenCredentialFactory(object): def decode(self, response, request): try: - creds = response.decode('base64') + creds = binascii.a2b_base64(response + b'===') except binascii.Error: raise error.LoginFailed('Invalid credentials') @@ -114,5 +130,5 @@ class TokenCredentialFactory(object): raise error.LoginFailed('Invalid credentials') -portal = Portal(SoledadRealm(), [TokenChecker()]) +get_portal = lambda: Portal(SoledadRealm(), [TokenChecker()]) credentialFactory = TokenCredentialFactory() diff --git a/server/src/leap/soledad/server/session.py b/server/src/leap/soledad/server/session.py index 4ed2721c..f1626115 100644 --- a/server/src/leap/soledad/server/session.py +++ b/server/src/leap/soledad/server/session.py @@ -26,7 +26,7 @@ from twisted.web.guard import HTTPAuthSessionWrapper from twisted.web.resource import ErrorPage from twisted.web.resource import IResource -from leap.soledad.server.auth import portal +from leap.soledad.server.auth import get_portal from leap.soledad.server.auth import credentialFactory from leap.soledad.server.url_mapper import URLMapper @@ -50,7 +50,7 @@ class SoledadSession(HTTPAuthSessionWrapper): def __init__(self): self._mapper = URLMapper() - self._portal = portal + self._portal = get_portal() self._credentialFactory = credentialFactory def _matchPath(self, request): -- cgit v1.2.3 From 911695e59ab60d2abaef3013330a6d41283cc733 Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 1 Feb 2017 18:16:43 -0200 Subject: [test] add tests for server auth session --- server/src/leap/soledad/server/session.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/session.py b/server/src/leap/soledad/server/session.py index f1626115..a56e4e97 100644 --- a/server/src/leap/soledad/server/session.py +++ b/server/src/leap/soledad/server/session.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # session.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2017 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 @@ -22,6 +22,7 @@ from zope.interface import implementer from twisted.cred import error from twisted.python import log from twisted.web import util +from twisted.web._auth import wrapper from twisted.web.guard import HTTPAuthSessionWrapper from twisted.web.resource import ErrorPage from twisted.web.resource import IResource @@ -32,9 +33,12 @@ from leap.soledad.server.url_mapper import URLMapper @implementer(IResource) -class UnauthorizedResource(object): +class UnauthorizedResource(wrapper.UnauthorizedResource): isLeaf = True + def __init__(self): + pass + def render(self, request): request.setResponseCode(401) if request.method == b'HEAD': @@ -48,9 +52,12 @@ class UnauthorizedResource(object): @implementer(IResource) class SoledadSession(HTTPAuthSessionWrapper): - def __init__(self): + def __init__(self, portal=None): + if portal is None: + portal = get_portal() + self._mapper = URLMapper() - self._portal = get_portal() + self._portal = portal self._credentialFactory = credentialFactory def _matchPath(self, request): @@ -65,18 +72,22 @@ class SoledadSession(HTTPAuthSessionWrapper): return None def _authorizedResource(self, request): + # check whether the path of the request exists in the app match = self._matchPath(request) if not match: return UnauthorizedResource() + # get authorization header or fail header = request.getHeader(b'authorization') if not header: return UnauthorizedResource() + # parse the authorization header auth_data = self._parseHeader(header) if not auth_data: return UnauthorizedResource() + # decode the credentials from the parsed header try: credentials = self._credentialFactory.decode(auth_data, request) except error.LoginFailed: @@ -85,10 +96,13 @@ class SoledadSession(HTTPAuthSessionWrapper): log.err(None, "Unexpected failure from credentials factory") return ErrorPage(500, None, None) + # make sure the uuid given in path corresponds to the one given in + # the credentials request_uuid = match.get('uuid') if request_uuid and request_uuid != credentials.username: return ErrorPage(500, None, None) + # if all checks pass, try to login with credentials return util.DeferredResource(self._login(credentials)) -- cgit v1.2.3 From c9cb1a814b6bfaa40de3c35a590f39d5fb0ce18e Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 1 Feb 2017 19:44:03 -0200 Subject: [test] fix session and auth tests --- server/src/leap/soledad/server/auth.py | 18 +++++++----------- server/src/leap/soledad/server/session.py | 3 --- 2 files changed, 7 insertions(+), 14 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index bcef2e7c..1f078bff 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -67,24 +67,20 @@ class TokenChecker(object): self._dbs = {} def _tokens_dbname(self): - dbname = self.TOKENS_DB_PREFIX + \ - str(int(time.time() / self.TOKENS_DB_EXPIRE)) - return dbname - - def _get_db(self, dbname): - if dbname not in self._dbs: - self._dbs[dbname] = self._server[dbname] - return self._dbs[dbname] - - def _tokens_db(self): # the tokens db rotates every 30 days, and the current db name is # "tokens_NNN", where NNN is the number of seconds since epoch # divide dby the rotate period in seconds. When rotating, old and # new tokens db coexist during a certain window of time and valid # tokens are replicated from the old db to the new one. See: # https://leap.se/code/issues/6785 + dbname = self.TOKENS_DB_PREFIX + \ + str(int(time.time() / self.TOKENS_DB_EXPIRE)) + return dbname + + def _tokens_db(self): dbname = self._tokens_dbname() - db = self._get_db(dbname) + with self._server as server: + db = server[dbname] return db def requestAvatarId(self, credentials): diff --git a/server/src/leap/soledad/server/session.py b/server/src/leap/soledad/server/session.py index a56e4e97..a2793bd3 100644 --- a/server/src/leap/soledad/server/session.py +++ b/server/src/leap/soledad/server/session.py @@ -104,6 +104,3 @@ class SoledadSession(HTTPAuthSessionWrapper): # if all checks pass, try to login with credentials return util.DeferredResource(self._login(credentials)) - - -soledadSession = SoledadSession() -- cgit v1.2.3 From 47c357213b4e39c6ced818de7eeb401e52bf92b9 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 26 Jan 2017 18:37:45 -0200 Subject: [refactor] move wsgi sync setup to its own module Conflicts: server/src/leap/soledad/server/_wsgi.py server/src/leap/soledad/server/entrypoint.py server/src/leap/soledad/server/resource.py testing/tests/server/test__resource.py --- server/src/leap/soledad/server/_resource.py | 42 ++++++++++++++ server/src/leap/soledad/server/_wsgi.py | 83 +++++++++++++++++++++++++++ server/src/leap/soledad/server/application.py | 77 ------------------------- server/src/leap/soledad/server/auth.py | 5 +- server/src/leap/soledad/server/entrypoint.py | 39 +++++++++++++ server/src/leap/soledad/server/resource.py | 52 ----------------- 6 files changed, 167 insertions(+), 131 deletions(-) create mode 100644 server/src/leap/soledad/server/_resource.py create mode 100644 server/src/leap/soledad/server/_wsgi.py delete mode 100644 server/src/leap/soledad/server/application.py create mode 100644 server/src/leap/soledad/server/entrypoint.py delete mode 100644 server/src/leap/soledad/server/resource.py (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_resource.py b/server/src/leap/soledad/server/_resource.py new file mode 100644 index 00000000..89e252d6 --- /dev/null +++ b/server/src/leap/soledad/server/_resource.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# resource.py +# Copyright (C) 2016 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 . +""" +A twisted resource that serves the Soledad Server. +""" +from twisted.web.resource import Resource + +from ._wsgi import sync_resource + + +__all__ = ['SoledadResource'] + + +class SoledadResource(Resource): + """ + This is a dummy twisted resource, used only to allow different entry points + for the Soledad Server. + """ + + def __init__(self): + self.children = {'': sync_resource} + + def getChild(self, path, request): + # for now, just "rewind" the path and serve the wsgi resource for all + # requests. In the future, we might look into the request path to + # decide which child resources should serve each request. + request.postpath.insert(0, request.prepath.pop()) + return self.children[''] diff --git a/server/src/leap/soledad/server/_wsgi.py b/server/src/leap/soledad/server/_wsgi.py new file mode 100644 index 00000000..13c8d13b --- /dev/null +++ b/server/src/leap/soledad/server/_wsgi.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# application.py +# Copyright (C) 2016 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 . +""" +A WSGI application to serve as the root resource of the webserver. + +Use it like this: + + twistd web --wsgi=leap.soledad.server.application.wsgi_application +""" +from twisted.internet import reactor +from twisted.python import threadpool +from twisted.web.wsgi import WSGIResource + +from leap.soledad.server import SoledadApp +from leap.soledad.server.gzip_middleware import GzipMiddleware +from leap.soledad.server.config import load_configuration +from leap.soledad.common.backend import SoledadBackend +from leap.soledad.common.couch.state import CouchServerState +from leap.soledad.common.log import getLogger + + +__all__ = ['init_couch_state', 'sync_resource'] + + +_config = None + + +def get_config(): + global _config + if not _config: + _config = load_configuration('/etc/soledad/soledad-server.conf') + return _config['soledad-server'] + + +def _get_couch_state(): + conf = get_config() + state = CouchServerState(conf['couch_url'], create_cmd=conf['create_cmd'], + check_schema_versions=True) + SoledadBackend.BATCH_SUPPORT = conf.get('batching', False) + return state + + +_app = SoledadApp(None) # delay state init +wsgi_application = GzipMiddleware(_app) + + +# During its initialization, the couch state verifies if all user databases +# contain a config document with the correct couch schema version stored, and +# will log an error and raise an exception if that is not the case. +# +# If this verification made too early (i.e. before the reactor has started and +# the twistd web logging facilities have been setup), the logging will not +# work. Because of that, we delay couch state initialization until the reactor +# is running. + +def init_couch_state(_app): + try: + _app.state = _get_couch_state() + except Exception as e: + logger = getLogger() + logger.error(str(e)) + reactor.stop() + + +# setup a wsgi resource with its own threadpool +pool = threadpool.ThreadPool() +reactor.callWhenRunning(pool.start) +reactor.addSystemEventTrigger('after', 'shutdown', pool.stop) +sync_resource = WSGIResource(reactor, pool, wsgi_application) diff --git a/server/src/leap/soledad/server/application.py b/server/src/leap/soledad/server/application.py deleted file mode 100644 index 8fd343b3..00000000 --- a/server/src/leap/soledad/server/application.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -# application.py -# Copyright (C) 2016 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 . -""" -A WSGI application to serve as the root resource of the webserver. - -Use it like this: - - twistd web --wsgi=leap.soledad.server.application.wsgi_application -""" -from twisted.internet import reactor - -from leap.soledad.server import SoledadApp -from leap.soledad.server.gzip_middleware import GzipMiddleware -from leap.soledad.server.config import load_configuration -from leap.soledad.common.backend import SoledadBackend -from leap.soledad.common.couch.state import CouchServerState -from leap.soledad.common.log import getLogger - - -__all__ = ['wsgi_application'] - - -_config = None - - -def get_config(): - global _config - if not _config: - _config = load_configuration('/etc/soledad/soledad-server.conf') - return _config['soledad-server'] - - -def _get_couch_state(): - conf = get_config() - state = CouchServerState(conf['couch_url'], create_cmd=conf['create_cmd'], - check_schema_versions=True) - SoledadBackend.BATCH_SUPPORT = conf.get('batching', False) - return state - - -_app = SoledadApp(None) # delay state init -wsgi_application = GzipMiddleware(_app) - - -# During its initialization, the couch state verifies if all user databases -# contain a config document with the correct couch schema version stored, and -# will log an error and raise an exception if that is not the case. -# -# If this verification made too early (i.e. before the reactor has started and -# the twistd web logging facilities have been setup), the logging will not -# work. Because of that, we delay couch state initialization until the reactor -# is running. - -def _init_couch_state(_app): - try: - _app.state = _get_couch_state() - except Exception as e: - logger = getLogger() - logger.error(str(e)) - reactor.stop() - - -reactor.callWhenRunning(_init_couch_state, _app) diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index 1f078bff..e616a94f 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -34,8 +34,9 @@ from twisted.web.iweb import ICredentialFactory from twisted.web.resource import IResource from leap.soledad.common.couch import couch_server -from leap.soledad.server.resource import SoledadResource -from leap.soledad.server.application import get_config + +from ._resource import SoledadResource +from ._wsgi import get_config @implementer(IRealm) diff --git a/server/src/leap/soledad/server/entrypoint.py b/server/src/leap/soledad/server/entrypoint.py new file mode 100644 index 00000000..df2b8934 --- /dev/null +++ b/server/src/leap/soledad/server/entrypoint.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# entrypoint.py +# Copyright (C) 2016 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 . +""" +The entrypoint for Soledad server. +""" +from twisted.internet import reactor + +from .config import load_configuration +from ._resource import SoledadResource +from ._wsgi import init_couch_state + + +# load configuration from file +conf = load_configuration('/etc/soledad/soledad-server.conf') + + +class SoledadEntrypoint(SoledadResource): + + def __init__(self): + SoledadResource.__init__(self, conf) + + +# see the comments in application.py recarding why couch state has to be +# initialized when the reactor is running +reactor.callWhenRunning(init_couch_state, conf['soledad-server']) diff --git a/server/src/leap/soledad/server/resource.py b/server/src/leap/soledad/server/resource.py deleted file mode 100644 index 67e9ae32..00000000 --- a/server/src/leap/soledad/server/resource.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# resource.py -# Copyright (C) 2016 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 . -""" -A twisted resource that serves the Soledad Server. -""" -from twisted.web.resource import Resource -from twisted.web.wsgi import WSGIResource -from twisted.internet import reactor -from twisted.python import threadpool - -from leap.soledad.server.application import wsgi_application - - -__all__ = ['SoledadResource'] - - -# setup a wsgi resource with its own threadpool -pool = threadpool.ThreadPool() -reactor.callWhenRunning(pool.start) -reactor.addSystemEventTrigger('after', 'shutdown', pool.stop) -wsgi_resource = WSGIResource(reactor, pool, wsgi_application) - - -class SoledadResource(Resource): - """ - This is a dummy twisted resource, used only to allow different entry points - for the Soledad Server. - """ - - def __init__(self): - self.children = {'': wsgi_resource} - - def getChild(self, path, request): - # for now, just "rewind" the path and serve the wsgi resource for all - # requests. In the future, we might look into the request path to - # decide which child resources should serve each request. - request.postpath.insert(0, request.prepath.pop()) - return self.children[''] -- cgit v1.2.3 From 47858d88ca4ca10ac363c71550b1bafe50f8f4ce Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 26 Jan 2017 19:18:15 -0200 Subject: [refactor] allow passing threadpool pool for server sync resource Conflicts: server/src/leap/soledad/server/_resource.py testing/tests/server/test__resource.py --- server/src/leap/soledad/server/_resource.py | 5 +++-- server/src/leap/soledad/server/_wsgi.py | 13 +++++++------ server/src/leap/soledad/server/auth.py | 13 +++++++++++-- server/src/leap/soledad/server/entrypoint.py | 11 +++++++---- 4 files changed, 28 insertions(+), 14 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_resource.py b/server/src/leap/soledad/server/_resource.py index 89e252d6..4070d786 100644 --- a/server/src/leap/soledad/server/_resource.py +++ b/server/src/leap/soledad/server/_resource.py @@ -19,7 +19,7 @@ A twisted resource that serves the Soledad Server. """ from twisted.web.resource import Resource -from ._wsgi import sync_resource +from ._wsgi import get_sync_resource __all__ = ['SoledadResource'] @@ -31,7 +31,8 @@ class SoledadResource(Resource): for the Soledad Server. """ - def __init__(self): + def __init__(self, sync_pool=None): + sync_resource = get_sync_resource(sync_pool) self.children = {'': sync_resource} def getChild(self, path, request): diff --git a/server/src/leap/soledad/server/_wsgi.py b/server/src/leap/soledad/server/_wsgi.py index 13c8d13b..3e30d560 100644 --- a/server/src/leap/soledad/server/_wsgi.py +++ b/server/src/leap/soledad/server/_wsgi.py @@ -33,7 +33,7 @@ from leap.soledad.common.couch.state import CouchServerState from leap.soledad.common.log import getLogger -__all__ = ['init_couch_state', 'sync_resource'] +__all__ = ['init_couch_state', 'get_sync_resource'] _config = None @@ -76,8 +76,9 @@ def init_couch_state(_app): reactor.stop() -# setup a wsgi resource with its own threadpool -pool = threadpool.ThreadPool() -reactor.callWhenRunning(pool.start) -reactor.addSystemEventTrigger('after', 'shutdown', pool.stop) -sync_resource = WSGIResource(reactor, pool, wsgi_application) +def get_sync_resource(pool=None): + if not pool: + pool = threadpool.ThreadPool() + reactor.callWhenRunning(pool.start) + reactor.addSystemEventTrigger('after', 'shutdown', pool.stop) + return WSGIResource(reactor, pool, wsgi_application) diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index e616a94f..a5d90c46 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -42,9 +42,13 @@ from ._wsgi import get_config @implementer(IRealm) class SoledadRealm(object): + def __init__(self, sync_pool=None): + self._sync_pool = sync_pool + def requestAvatar(self, avatarId, mind, *interfaces): if IResource in interfaces: - return (IResource, SoledadResource(), lambda: None) + resource = SoledadResource(sync_pool=self._sync_pool) + return (IResource, resource, lambda: None) raise NotImplementedError() @@ -127,5 +131,10 @@ class TokenCredentialFactory(object): raise error.LoginFailed('Invalid credentials') -get_portal = lambda: Portal(SoledadRealm(), [TokenChecker()]) +def get_portal(sync_pool=None): + realm = SoledadRealm(sync_pool=sync_pool) + checker = TokenChecker() + return Portal(realm, [checker]) + + credentialFactory = TokenCredentialFactory() diff --git a/server/src/leap/soledad/server/entrypoint.py b/server/src/leap/soledad/server/entrypoint.py index df2b8934..7501a447 100644 --- a/server/src/leap/soledad/server/entrypoint.py +++ b/server/src/leap/soledad/server/entrypoint.py @@ -20,7 +20,7 @@ The entrypoint for Soledad server. from twisted.internet import reactor from .config import load_configuration -from ._resource import SoledadResource +from ._session import SoledadSession from ._wsgi import init_couch_state @@ -28,10 +28,13 @@ from ._wsgi import init_couch_state conf = load_configuration('/etc/soledad/soledad-server.conf') -class SoledadEntrypoint(SoledadResource): +class SoledadEntrypoint(SoledadSession): - def __init__(self): - SoledadResource.__init__(self, conf) + # the purpose of the entrypoint is to avoid trying to load the + # configuration file during tests. This class will be more useful when we + # add the blobs feature toggle. For now, the whole entrypoint + + pass # see the comments in application.py recarding why couch state has to be -- cgit v1.2.3 From 02764109fa1145474c24b73d537280e3a5652f78 Mon Sep 17 00:00:00 2001 From: Thais Siqueira Date: Wed, 11 Jan 2017 15:26:11 -0200 Subject: [bug] Fix import for load_configuration on migration script --- server/src/leap/soledad/server/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/__init__.py b/server/src/leap/soledad/server/__init__.py index 039bef75..5bed22c9 100644 --- a/server/src/leap/soledad/server/__init__.py +++ b/server/src/leap/soledad/server/__init__.py @@ -88,15 +88,17 @@ import sys from leap.soledad.common.l2db.remote import http_app, utils from leap.soledad.common import SHARED_DB_NAME -from leap.soledad.server.sync import SyncResource -from leap.soledad.server.sync import MAX_REQUEST_SIZE -from leap.soledad.server.sync import MAX_ENTRY_SIZE +from .sync import SyncResource +from .sync import MAX_REQUEST_SIZE +from .sync import MAX_ENTRY_SIZE from ._version import get_versions +from ._config import get_config __all__ = [ 'SoledadApp', + 'get_config', '__version__', ] -- cgit v1.2.3 From 0e12cd3eb1e20bb867f34e0bf60f280d93b6182d Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 18 Jan 2017 17:28:01 -0200 Subject: [feature] add server config option for blobs --- server/src/leap/soledad/server/_blobs.py | 34 ++++++++++++ server/src/leap/soledad/server/_config.py | 81 ++++++++++++++++++++++++++++ server/src/leap/soledad/server/_resource.py | 22 ++++++-- server/src/leap/soledad/server/_wsgi.py | 18 ++----- server/src/leap/soledad/server/auth.py | 2 +- server/src/leap/soledad/server/config.py | 67 ----------------------- server/src/leap/soledad/server/entrypoint.py | 4 +- 7 files changed, 138 insertions(+), 90 deletions(-) create mode 100644 server/src/leap/soledad/server/_blobs.py create mode 100644 server/src/leap/soledad/server/_config.py delete mode 100644 server/src/leap/soledad/server/config.py (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_blobs.py b/server/src/leap/soledad/server/_blobs.py new file mode 100644 index 00000000..0424aae0 --- /dev/null +++ b/server/src/leap/soledad/server/_blobs.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# _blobs.py +# Copyright (C) 2017 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 . +""" +Blobs Server implementation. +""" +from twisted.web import resource + + +__all__ = ['blobs_resource'] + + +class BlobsResource(resource.Resource): + + isLeaf = True + + def render_GET(self, request): + return 'blobs is not implemented yet!' + + +blobs_resource = BlobsResource() diff --git a/server/src/leap/soledad/server/_config.py b/server/src/leap/soledad/server/_config.py new file mode 100644 index 00000000..2c7f530d --- /dev/null +++ b/server/src/leap/soledad/server/_config.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# config.py +# Copyright (C) 2016 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 . + + +import configparser + + +__all__ = ['get_config'] + + +CONFIG_DEFAULTS = { + 'soledad-server': { + 'couch_url': 'http://localhost:5984', + 'create_cmd': None, + 'admin_netrc': '/etc/couchdb/couchdb-admin.netrc', + 'batching': True, + 'blobs': False, + }, + 'database-security': { + 'members': ['soledad'], + 'members_roles': [], + 'admins': [], + 'admins_roles': [] + } +} + + +_config = None + + +def get_config(): + global _config + if not _config: + _config = _load_config('/etc/soledad/soledad-server.conf') + return _config['soledad-server'] + + +def _load_config(file_path): + """ + Load server configuration from file. + + @param file_path: The path to the configuration file. + @type file_path: str + + @return: A dictionary with the configuration. + @rtype: dict + """ + conf = dict(CONFIG_DEFAULTS) + config = configparser.SafeConfigParser() + config.read(file_path) + for section in conf: + if not config.has_section(section): + continue + for key, value in conf[section].items(): + if not config.has_option(section, key): + continue + elif type(value) == bool: + conf[section][key] = config.getboolean(section, key) + elif type(value) == list: + values = config.get(section, key).split(',') + values = [v.strip() for v in values] + conf[section][key] = values + else: + conf[section][key] = config.get(section, key) + # TODO: implement basic parsing/sanitization of options comming from + # config file. + return conf diff --git a/server/src/leap/soledad/server/_resource.py b/server/src/leap/soledad/server/_resource.py index 4070d786..4f8e98df 100644 --- a/server/src/leap/soledad/server/_resource.py +++ b/server/src/leap/soledad/server/_resource.py @@ -17,8 +17,10 @@ """ A twisted resource that serves the Soledad Server. """ +from twisted.web.error import Error from twisted.web.resource import Resource +from ._blobs import blobs_resource from ._wsgi import get_sync_resource @@ -33,11 +35,21 @@ class SoledadResource(Resource): def __init__(self, sync_pool=None): sync_resource = get_sync_resource(sync_pool) - self.children = {'': sync_resource} + self.children = { + 'sync': sync_resource, + 'blobs': blobs_resource, + } def getChild(self, path, request): - # for now, just "rewind" the path and serve the wsgi resource for all - # requests. In the future, we might look into the request path to - # decide which child resources should serve each request. + """ + Decide which child resource to serve based on the given path. + """ + if path == 'blobs': + if not self._blobs_enabled: + msg = 'Blobs feature is disabled in this server.' + raise Error(403, message=msg) + return self.children['blobs'] + + # rewind the path and serve the wsgi sync resource request.postpath.insert(0, request.prepath.pop()) - return self.children[''] + return self.children['sync'] diff --git a/server/src/leap/soledad/server/_wsgi.py b/server/src/leap/soledad/server/_wsgi.py index 3e30d560..c00d00ae 100644 --- a/server/src/leap/soledad/server/_wsgi.py +++ b/server/src/leap/soledad/server/_wsgi.py @@ -27,7 +27,6 @@ from twisted.web.wsgi import WSGIResource from leap.soledad.server import SoledadApp from leap.soledad.server.gzip_middleware import GzipMiddleware -from leap.soledad.server.config import load_configuration from leap.soledad.common.backend import SoledadBackend from leap.soledad.common.couch.state import CouchServerState from leap.soledad.common.log import getLogger @@ -36,18 +35,7 @@ from leap.soledad.common.log import getLogger __all__ = ['init_couch_state', 'get_sync_resource'] -_config = None - - -def get_config(): - global _config - if not _config: - _config = load_configuration('/etc/soledad/soledad-server.conf') - return _config['soledad-server'] - - -def _get_couch_state(): - conf = get_config() +def _get_couch_state(conf): state = CouchServerState(conf['couch_url'], create_cmd=conf['create_cmd'], check_schema_versions=True) SoledadBackend.BATCH_SUPPORT = conf.get('batching', False) @@ -67,9 +55,9 @@ wsgi_application = GzipMiddleware(_app) # work. Because of that, we delay couch state initialization until the reactor # is running. -def init_couch_state(_app): +def init_couch_state(conf): try: - _app.state = _get_couch_state() + _app.state = _get_couch_state(conf) except Exception as e: logger = getLogger() logger.error(str(e)) diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index a5d90c46..d7ccdeb9 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -36,7 +36,7 @@ from twisted.web.resource import IResource from leap.soledad.common.couch import couch_server from ._resource import SoledadResource -from ._wsgi import get_config +from ._config import get_config @implementer(IRealm) diff --git a/server/src/leap/soledad/server/config.py b/server/src/leap/soledad/server/config.py deleted file mode 100644 index 3c17ec19..00000000 --- a/server/src/leap/soledad/server/config.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -# config.py -# Copyright (C) 2016 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 . - - -import configparser - - -CONFIG_DEFAULTS = { - 'soledad-server': { - 'couch_url': 'http://localhost:5984', - 'create_cmd': None, - 'admin_netrc': '/etc/couchdb/couchdb-admin.netrc', - 'batching': True - }, - 'database-security': { - 'members': ['soledad'], - 'members_roles': [], - 'admins': [], - 'admins_roles': [] - } -} - - -def load_configuration(file_path): - """ - Load server configuration from file. - - @param file_path: The path to the configuration file. - @type file_path: str - - @return: A dictionary with the configuration. - @rtype: dict - """ - defaults = dict(CONFIG_DEFAULTS) - config = configparser.SafeConfigParser() - config.read(file_path) - for section in defaults: - if not config.has_section(section): - continue - for key, value in defaults[section].items(): - if not config.has_option(section, key): - continue - elif type(value) == bool: - defaults[section][key] = config.getboolean(section, key) - elif type(value) == list: - values = config.get(section, key).split(',') - values = [v.strip() for v in values] - defaults[section][key] = values - else: - defaults[section][key] = config.get(section, key) - # TODO: implement basic parsing/sanitization of options comming from - # config file. - return defaults diff --git a/server/src/leap/soledad/server/entrypoint.py b/server/src/leap/soledad/server/entrypoint.py index 7501a447..714490ae 100644 --- a/server/src/leap/soledad/server/entrypoint.py +++ b/server/src/leap/soledad/server/entrypoint.py @@ -19,13 +19,13 @@ The entrypoint for Soledad server. """ from twisted.internet import reactor -from .config import load_configuration +from ._config import get_config from ._session import SoledadSession from ._wsgi import init_couch_state # load configuration from file -conf = load_configuration('/etc/soledad/soledad-server.conf') +conf = get_config class SoledadEntrypoint(SoledadSession): -- cgit v1.2.3 From ce0aed6d832f2f49c9306c8ccb6dd128cca1c511 Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 18 Jan 2017 17:28:01 -0200 Subject: [feature] add server config option for blobs --- server/src/leap/soledad/server/_resource.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_resource.py b/server/src/leap/soledad/server/_resource.py index 4f8e98df..3e307e44 100644 --- a/server/src/leap/soledad/server/_resource.py +++ b/server/src/leap/soledad/server/_resource.py @@ -21,6 +21,7 @@ from twisted.web.error import Error from twisted.web.resource import Resource from ._blobs import blobs_resource +from ._config import get_config from ._wsgi import get_sync_resource @@ -33,8 +34,11 @@ class SoledadResource(Resource): for the Soledad Server. """ + _conf = get_config() + def __init__(self, sync_pool=None): sync_resource = get_sync_resource(sync_pool) + self._blobs_enabled = self._conf['soledad-server']['blobs'] self.children = { 'sync': sync_resource, 'blobs': blobs_resource, -- cgit v1.2.3 From 6793ff931a5fea247d5bd1634d48a789f5e7d845 Mon Sep 17 00:00:00 2001 From: drebs Date: Sun, 22 Jan 2017 19:36:42 -0200 Subject: [refactor] rename server auth classes --- server/src/leap/soledad/server/_wsgi.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_wsgi.py b/server/src/leap/soledad/server/_wsgi.py index c00d00ae..37a03ced 100644 --- a/server/src/leap/soledad/server/_wsgi.py +++ b/server/src/leap/soledad/server/_wsgi.py @@ -15,11 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ -A WSGI application to serve as the root resource of the webserver. - -Use it like this: - - twistd web --wsgi=leap.soledad.server.application.wsgi_application +A WSGI application that serves Soledad synchronization. """ from twisted.internet import reactor from twisted.python import threadpool -- cgit v1.2.3 From accd15d5842640ba4cd9a5a1e6024a2ef5f7eb06 Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 23 Jan 2017 18:53:16 -0200 Subject: [feature] announce server blobs capabilities - add a new ServerInfo resource for / - move entrypoint to its own module --- server/src/leap/soledad/server/_resource.py | 16 +++++++--- server/src/leap/soledad/server/_server_info.py | 41 ++++++++++++++++++++++++++ server/src/leap/soledad/server/entrypoint.py | 7 ++--- 3 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 server/src/leap/soledad/server/_server_info.py (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_resource.py b/server/src/leap/soledad/server/_resource.py index 3e307e44..1c4edade 100644 --- a/server/src/leap/soledad/server/_resource.py +++ b/server/src/leap/soledad/server/_resource.py @@ -22,6 +22,7 @@ from twisted.web.resource import Resource from ._blobs import blobs_resource from ._config import get_config +from ._server_info import ServerInfo from ._wsgi import get_sync_resource @@ -34,26 +35,33 @@ class SoledadResource(Resource): for the Soledad Server. """ - _conf = get_config() - def __init__(self, sync_pool=None): sync_resource = get_sync_resource(sync_pool) - self._blobs_enabled = self._conf['soledad-server']['blobs'] + conf = get_config() + self._blobs_enabled = conf['soledad-server']['blobs'] + server_info = ServerInfo(self._blobs_enabled) self.children = { + '': server_info, 'sync': sync_resource, 'blobs': blobs_resource, + 'sync': sync_resource, } def getChild(self, path, request): """ Decide which child resource to serve based on the given path. """ + # requests to / return server information + if path == '': + return self.children[''] + + # requests to /blobs will serve blobs if enabled if path == 'blobs': if not self._blobs_enabled: msg = 'Blobs feature is disabled in this server.' raise Error(403, message=msg) return self.children['blobs'] - # rewind the path and serve the wsgi sync resource + # other requesta are routed to legacy sync resource request.postpath.insert(0, request.prepath.pop()) return self.children['sync'] diff --git a/server/src/leap/soledad/server/_server_info.py b/server/src/leap/soledad/server/_server_info.py new file mode 100644 index 00000000..a1dd3555 --- /dev/null +++ b/server/src/leap/soledad/server/_server_info.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# _server_info.py +# Copyright (C) 2017 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 . +""" +Resource that announces information about the server. +""" +import json + +from twisted.web.resource import Resource + + +__all__ = ['ServerInfo'] + + +class ServerInfo(Resource): + """ + Return information about the server. + """ + + isLeaf = True + + def __init__(self, blobs_enabled): + self._info = { + "blobs": blobs_enabled, + } + + def render_GET(self, request): + return json.dumps(self._info) diff --git a/server/src/leap/soledad/server/entrypoint.py b/server/src/leap/soledad/server/entrypoint.py index 714490ae..cfc557a3 100644 --- a/server/src/leap/soledad/server/entrypoint.py +++ b/server/src/leap/soledad/server/entrypoint.py @@ -30,11 +30,8 @@ conf = get_config class SoledadEntrypoint(SoledadSession): - # the purpose of the entrypoint is to avoid trying to load the - # configuration file during tests. This class will be more useful when we - # add the blobs feature toggle. For now, the whole entrypoint - - pass + def __init__(self): + SoledadSession.__init__(self, conf) # see the comments in application.py recarding why couch state has to be -- cgit v1.2.3 From 1f1a9847117d23a767e99fe80484baf232375d36 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 26 Jan 2017 19:18:15 -0200 Subject: [refactor] allow passing threadpool pool for server sync resource --- server/src/leap/soledad/server/_resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_resource.py b/server/src/leap/soledad/server/_resource.py index 1c4edade..046e20d1 100644 --- a/server/src/leap/soledad/server/_resource.py +++ b/server/src/leap/soledad/server/_resource.py @@ -36,10 +36,10 @@ class SoledadResource(Resource): """ def __init__(self, sync_pool=None): - sync_resource = get_sync_resource(sync_pool) conf = get_config() self._blobs_enabled = conf['soledad-server']['blobs'] server_info = ServerInfo(self._blobs_enabled) + sync_resource = get_sync_resource(sync_pool) self.children = { '': server_info, 'sync': sync_resource, -- cgit v1.2.3 From 53b5a6788ad8416f78b24cc9880d02da73c52d70 Mon Sep 17 00:00:00 2001 From: drebs Date: Fri, 27 Jan 2017 20:30:02 -0200 Subject: [refacor] make proper use of twisted web dyamic resources in server --- server/src/leap/soledad/server/_resource.py | 42 +++++++++++------------------ 1 file changed, 16 insertions(+), 26 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_resource.py b/server/src/leap/soledad/server/_resource.py index 046e20d1..fec290a2 100644 --- a/server/src/leap/soledad/server/_resource.py +++ b/server/src/leap/soledad/server/_resource.py @@ -17,11 +17,9 @@ """ A twisted resource that serves the Soledad Server. """ -from twisted.web.error import Error from twisted.web.resource import Resource from ._blobs import blobs_resource -from ._config import get_config from ._server_info import ServerInfo from ._wsgi import get_sync_resource @@ -35,33 +33,25 @@ class SoledadResource(Resource): for the Soledad Server. """ - def __init__(self, sync_pool=None): - conf = get_config() - self._blobs_enabled = conf['soledad-server']['blobs'] - server_info = ServerInfo(self._blobs_enabled) - sync_resource = get_sync_resource(sync_pool) - self.children = { - '': server_info, - 'sync': sync_resource, - 'blobs': blobs_resource, - 'sync': sync_resource, - } + def __init__(self, conf, sync_pool=None): + Resource.__init__(self) + + blobs_enabled = conf['soledad-server']['blobs'] - def getChild(self, path, request): - """ - Decide which child resource to serve based on the given path. - """ # requests to / return server information - if path == '': - return self.children[''] + server_info = ServerInfo(blobs_enabled) + self.putChild('', server_info) # requests to /blobs will serve blobs if enabled - if path == 'blobs': - if not self._blobs_enabled: - msg = 'Blobs feature is disabled in this server.' - raise Error(403, message=msg) - return self.children['blobs'] + if blobs_enabled: + self.putChild('blobs', blobs_resource) - # other requesta are routed to legacy sync resource + # other requests are routed to legacy sync resource + self._sync_resource = get_sync_resource(sync_pool) + + def getChild(self, path, request): + """ + Route requests to legacy WSGI sync resource dynamically. + """ request.postpath.insert(0, request.prepath.pop()) - return self.children['sync'] + return self._sync_resource -- cgit v1.2.3 From a2f041de7f1ea653f078ac9cd532e2d2b774248f Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 2 Feb 2017 11:45:57 -0200 Subject: [refactor] parametrize blobs toggling in soledad server resource --- server/src/leap/soledad/server/_resource.py | 17 ++++++++++++----- server/src/leap/soledad/server/auth.py | 10 ++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_resource.py b/server/src/leap/soledad/server/_resource.py index fec290a2..156e18aa 100644 --- a/server/src/leap/soledad/server/_resource.py +++ b/server/src/leap/soledad/server/_resource.py @@ -33,17 +33,24 @@ class SoledadResource(Resource): for the Soledad Server. """ - def __init__(self, conf, sync_pool=None): - Resource.__init__(self) + def __init__(self, enable_blobs=False, sync_pool=None): + """ + Initialize the Soledad resource. + + :param enable_blobs: Whether the blobs feature should be enabled. + :type enable_blobs: bool - blobs_enabled = conf['soledad-server']['blobs'] + :param sync_pool: A pool to pass to the WSGI sync resource. + :type sync_pool: twisted.python.threadpool.ThreadPool + """ + Resource.__init__(self) # requests to / return server information - server_info = ServerInfo(blobs_enabled) + server_info = ServerInfo(enable_blobs) self.putChild('', server_info) # requests to /blobs will serve blobs if enabled - if blobs_enabled: + if enable_blobs: self.putChild('blobs', blobs_resource) # other requests are routed to legacy sync resource diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index d7ccdeb9..0ec890ca 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -42,12 +42,18 @@ from ._config import get_config @implementer(IRealm) class SoledadRealm(object): - def __init__(self, sync_pool=None): + def __init__(self, conf=None, sync_pool=None): + if not conf: + conf = get_config() + self._conf = conf self._sync_pool = sync_pool def requestAvatar(self, avatarId, mind, *interfaces): if IResource in interfaces: - resource = SoledadResource(sync_pool=self._sync_pool) + enable_blobs = self._conf['soledad-server']['blobs'] + resource = SoledadResource( + enable_blobs=enable_blobs, + sync_pool=self._sync_pool) return (IResource, resource, lambda: None) raise NotImplementedError() -- cgit v1.2.3 From c5388e464907ab0b7192d269173d632af14ae4f1 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 10 Feb 2017 00:16:56 +0100 Subject: [bug] fix import for the session module --- server/src/leap/soledad/server/entrypoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/entrypoint.py b/server/src/leap/soledad/server/entrypoint.py index cfc557a3..0bb1c854 100644 --- a/server/src/leap/soledad/server/entrypoint.py +++ b/server/src/leap/soledad/server/entrypoint.py @@ -20,8 +20,8 @@ The entrypoint for Soledad server. from twisted.internet import reactor from ._config import get_config -from ._session import SoledadSession from ._wsgi import init_couch_state +from .session import SoledadSession # load configuration from file -- cgit v1.2.3 From 8a1cbb7bd1ea79408e9ca8f777277d1155f47504 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 10 Feb 2017 01:57:26 +0100 Subject: [bug] effectively load the configuration for the app the code for passing the configuration to the couch initialization was never called. it seems the entrypoint module wasn't finally hooked as expected. I think this fixes the problem, but further review is needed here: either the entrypoint module is to be used, or it better is removed. in the first case, this workaround probably needs to be reverted. --- server/src/leap/soledad/server/_wsgi.py | 10 ++++++++++ server/src/leap/soledad/server/entrypoint.py | 7 +++++++ 2 files changed, 17 insertions(+) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_wsgi.py b/server/src/leap/soledad/server/_wsgi.py index 37a03ced..f0961eaf 100644 --- a/server/src/leap/soledad/server/_wsgi.py +++ b/server/src/leap/soledad/server/_wsgi.py @@ -27,6 +27,7 @@ from leap.soledad.common.backend import SoledadBackend from leap.soledad.common.couch.state import CouchServerState from leap.soledad.common.log import getLogger +from ._config import get_config __all__ = ['init_couch_state', 'get_sync_resource'] @@ -66,3 +67,12 @@ def get_sync_resource(pool=None): reactor.callWhenRunning(pool.start) reactor.addSystemEventTrigger('after', 'shutdown', pool.stop) return WSGIResource(reactor, pool, wsgi_application) + + +# load configuration from file +conf = get_config() + +# see the comments in application.py recarding why couch state has to be +# initialized when the reactor is running + +reactor.callWhenRunning(init_couch_state, conf) diff --git a/server/src/leap/soledad/server/entrypoint.py b/server/src/leap/soledad/server/entrypoint.py index 0bb1c854..9cc1f97b 100644 --- a/server/src/leap/soledad/server/entrypoint.py +++ b/server/src/leap/soledad/server/entrypoint.py @@ -17,6 +17,7 @@ """ The entrypoint for Soledad server. """ + from twisted.internet import reactor from ._config import get_config @@ -34,6 +35,12 @@ class SoledadEntrypoint(SoledadSession): SoledadSession.__init__(self, conf) +# XXX FIXME ---------------------------- +# this is not executed from anywhere. +# what's the plan for this module? +# use me, or delete me. +# -------------------------------------- # see the comments in application.py recarding why couch state has to be # initialized when the reactor is running + reactor.callWhenRunning(init_couch_state, conf['soledad-server']) -- cgit v1.2.3 From e095cd3f00a06bf0e3705b28d01b49b61388ef85 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 10 Feb 2017 22:46:15 +0100 Subject: [bug] revert loading from the wsgi entrypoint --- server/src/leap/soledad/server/_wsgi.py | 10 ---------- 1 file changed, 10 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_wsgi.py b/server/src/leap/soledad/server/_wsgi.py index f0961eaf..37a03ced 100644 --- a/server/src/leap/soledad/server/_wsgi.py +++ b/server/src/leap/soledad/server/_wsgi.py @@ -27,7 +27,6 @@ from leap.soledad.common.backend import SoledadBackend from leap.soledad.common.couch.state import CouchServerState from leap.soledad.common.log import getLogger -from ._config import get_config __all__ = ['init_couch_state', 'get_sync_resource'] @@ -67,12 +66,3 @@ def get_sync_resource(pool=None): reactor.callWhenRunning(pool.start) reactor.addSystemEventTrigger('after', 'shutdown', pool.stop) return WSGIResource(reactor, pool, wsgi_application) - - -# load configuration from file -conf = get_config() - -# see the comments in application.py recarding why couch state has to be -# initialized when the reactor is running - -reactor.callWhenRunning(init_couch_state, conf) -- cgit v1.2.3 From 05b604a7134866c23aa98069cfc8542feaa08404 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 10 Feb 2017 22:47:03 +0100 Subject: [bug] fix config handling after refactor --- server/src/leap/soledad/server/auth.py | 2 +- server/src/leap/soledad/server/entrypoint.py | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index 0ec890ca..c5954c60 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -50,7 +50,7 @@ class SoledadRealm(object): def requestAvatar(self, avatarId, mind, *interfaces): if IResource in interfaces: - enable_blobs = self._conf['soledad-server']['blobs'] + enable_blobs = self._conf['blobs'] resource = SoledadResource( enable_blobs=enable_blobs, sync_pool=self._sync_pool) diff --git a/server/src/leap/soledad/server/entrypoint.py b/server/src/leap/soledad/server/entrypoint.py index 9cc1f97b..8078a54a 100644 --- a/server/src/leap/soledad/server/entrypoint.py +++ b/server/src/leap/soledad/server/entrypoint.py @@ -16,6 +16,9 @@ # along with this program. If not, see . """ The entrypoint for Soledad server. + +This is the entrypoint for the application that is loaded from the initscript +or the systemd script. """ from twisted.internet import reactor @@ -26,21 +29,16 @@ from .session import SoledadSession # load configuration from file -conf = get_config +conf = get_config() class SoledadEntrypoint(SoledadSession): def __init__(self): - SoledadSession.__init__(self, conf) + SoledadSession.__init__(self) -# XXX FIXME ---------------------------- -# this is not executed from anywhere. -# what's the plan for this module? -# use me, or delete me. -# -------------------------------------- # see the comments in application.py recarding why couch state has to be # initialized when the reactor is running -reactor.callWhenRunning(init_couch_state, conf['soledad-server']) +reactor.callWhenRunning(init_couch_state, conf) -- cgit v1.2.3 From 6cabe46e4671627c22d5eed9ebb3bdc751948414 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Sun, 12 Feb 2017 20:33:48 +0100 Subject: [refactor] update create-user-db script to use config refactor --- server/src/leap/soledad/server/_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_config.py b/server/src/leap/soledad/server/_config.py index 2c7f530d..1818c38d 100644 --- a/server/src/leap/soledad/server/_config.py +++ b/server/src/leap/soledad/server/_config.py @@ -42,11 +42,11 @@ CONFIG_DEFAULTS = { _config = None -def get_config(): +def get_config(section='soledad-server'): global _config if not _config: _config = _load_config('/etc/soledad/soledad-server.conf') - return _config['soledad-server'] + return _config[section] def _load_config(file_path): -- cgit v1.2.3 From 1a5b292bd1fbd57b0b3127857e74bdf1ac22a7c6 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Sun, 12 Feb 2017 20:40:01 +0100 Subject: [bug] get a new server instance on each request to the tokens db --- server/src/leap/soledad/server/auth.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index c5954c60..7112aa35 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -69,13 +69,11 @@ class TokenChecker(object): TOKENS_TYPE_DEF = "Token" TOKENS_USER_ID_KEY = "user_id" - def __init__(self, server=None): - if server is None: - config = get_config() - couch_url = config['couch_url'] - server = couch_server(couch_url) - self._server = server - self._dbs = {} + def __init__(self): + self._couch_url = get_config().get('couch_url') + + def _get_server(self): + return couch_server(self._couch_url) def _tokens_dbname(self): # the tokens db rotates every 30 days, and the current db name is @@ -90,7 +88,11 @@ class TokenChecker(object): def _tokens_db(self): dbname = self._tokens_dbname() - with self._server as server: + + # TODO -- leaking abstraction here: this module shouldn't need + # to known anything about the context manager. hide that in the couch + # module + with self._get_server() as server: db = server[dbname] return db @@ -99,11 +101,14 @@ class TokenChecker(object): token = credentials.password # lookup key is a hash of the token to prevent timing attacks. + # TODO cache the tokens already! + db = self._tokens_db() token = db.get(sha512(token).hexdigest()) if token is None: return defer.fail(error.UnauthorizedLogin()) + # TODO -- use cryptography constant time builtin comparison. # we compare uuid hashes to avoid possible timing attacks that # might exploit python's builtin comparison operator behaviour, # which fails immediatelly when non-matching bytes are found. -- cgit v1.2.3 From 4d26c3c6ee43f110f30e0fb1c14d93f847c3081b Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 13 Feb 2017 14:53:25 +0100 Subject: [bug] add expected attribute to authentication wrapper in entrypoint the authentication wrapper is goin to look for the _credentialFactories attribute. it will raise an exception if not found - Resolves: #8766 --- server/src/leap/soledad/server/entrypoint.py | 1 + 1 file changed, 1 insertion(+) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/entrypoint.py b/server/src/leap/soledad/server/entrypoint.py index 8078a54a..b10bfed6 100644 --- a/server/src/leap/soledad/server/entrypoint.py +++ b/server/src/leap/soledad/server/entrypoint.py @@ -35,6 +35,7 @@ conf = get_config() class SoledadEntrypoint(SoledadSession): def __init__(self): + self._credentialFactories = [] SoledadSession.__init__(self) -- cgit v1.2.3 From e4a9127914dac8cf2cd8b8f7a4e48bbcf1de7a5d Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 14 Feb 2017 23:31:36 +0100 Subject: [feature] add version to the banner --- server/src/leap/soledad/server/_server_info.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_server_info.py b/server/src/leap/soledad/server/_server_info.py index a1dd3555..50659338 100644 --- a/server/src/leap/soledad/server/_server_info.py +++ b/server/src/leap/soledad/server/_server_info.py @@ -21,6 +21,8 @@ import json from twisted.web.resource import Resource +from leap.soledad.server import __version__ + __all__ = ['ServerInfo'] @@ -35,6 +37,7 @@ class ServerInfo(Resource): def __init__(self, blobs_enabled): self._info = { "blobs": blobs_enabled, + "version": __version__ } def render_GET(self, request): -- cgit v1.2.3 From ccb280703ba851265702b8a92cdedb294cc93608 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 14 Feb 2017 23:40:29 +0100 Subject: [feature] authenticate as anonymous if no token in header and serve / banner and robots to anon users. instead of returning 401 for all cases, I treat the unauthenticated case as a special case, and switch the service tree apart. this allows to serve a different resource tree to unauthenticated users. the new URLs are registered with the mapper. I don't really like that dependency, could be handled by twisted alone, but meh. - Resolves: #8764 --- server/src/leap/soledad/server/_resource.py | 19 ++++++++++++++++++- server/src/leap/soledad/server/auth.py | 26 +++++++++++++++++++++++--- server/src/leap/soledad/server/session.py | 8 ++++++-- server/src/leap/soledad/server/url_mapper.py | 3 +++ 4 files changed, 50 insertions(+), 6 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_resource.py b/server/src/leap/soledad/server/_resource.py index 156e18aa..e04c0708 100644 --- a/server/src/leap/soledad/server/_resource.py +++ b/server/src/leap/soledad/server/_resource.py @@ -24,7 +24,24 @@ from ._server_info import ServerInfo from ._wsgi import get_sync_resource -__all__ = ['SoledadResource'] +__all__ = ['SoledadResource', 'SoledadAnonResource'] + + +class Robots(Resource): + def render_GET(self, request): + return 'robots, go away! please!' + + +class SoledadAnonResource(Resource): + """ + The parts of Soledad Server that unauthenticated users can see + """ + + def __init__(self, enable_blobs=False): + Resource.__init__(self) + server_info = ServerInfo(enable_blobs) + self.putChild('', server_info) + self.putChild('robots.txt', Robots()) class SoledadResource(Resource): diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index 7112aa35..6ce11e71 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -26,19 +26,25 @@ from zope.interface import implementer from twisted.cred import error from twisted.cred.checkers import ICredentialsChecker from twisted.cred.credentials import IUsernamePassword +from twisted.cred.credentials import IAnonymous +from twisted.cred.credentials import Anonymous from twisted.cred.credentials import UsernamePassword from twisted.cred.portal import IRealm from twisted.cred.portal import Portal +from twisted.logger import Logger from twisted.internet import defer from twisted.web.iweb import ICredentialFactory from twisted.web.resource import IResource from leap.soledad.common.couch import couch_server -from ._resource import SoledadResource +from ._resource import SoledadResource, SoledadAnonResource from ._config import get_config +log = Logger() + + @implementer(IRealm) class SoledadRealm(object): @@ -49,8 +55,17 @@ class SoledadRealm(object): self._sync_pool = sync_pool def requestAvatar(self, avatarId, mind, *interfaces): + log.warn('avatarId {0}'.format(avatarId)) + enable_blobs = self._conf['blobs'] + + # Anonymous access + if IAnonymous.providedBy(avatarId): + resource = SoledadAnonResource( + enable_blobs=enable_blobs) + return (IResource, resource, lambda: None) + + # Authenticated users if IResource in interfaces: - enable_blobs = self._conf['blobs'] resource = SoledadResource( enable_blobs=enable_blobs, sync_pool=self._sync_pool) @@ -61,7 +76,7 @@ class SoledadRealm(object): @implementer(ICredentialsChecker) class TokenChecker(object): - credentialInterfaces = [IUsernamePassword] + credentialInterfaces = [IUsernamePassword, IAnonymous] TOKENS_DB_PREFIX = "tokens_" TOKENS_DB_EXPIRE = 30 * 24 * 3600 # 30 days in seconds @@ -97,6 +112,10 @@ class TokenChecker(object): return db def requestAvatarId(self, credentials): + if IAnonymous.providedBy(credentials): + log.warn('we are anon') + return defer.succeed(Anonymous()) + uuid = credentials.username token = credentials.password @@ -106,6 +125,7 @@ class TokenChecker(object): db = self._tokens_db() token = db.get(sha512(token).hexdigest()) if token is None: + log.warn('token is none') return defer.fail(error.UnauthorizedLogin()) # TODO -- use cryptography constant time builtin comparison. diff --git a/server/src/leap/soledad/server/session.py b/server/src/leap/soledad/server/session.py index a2793bd3..70e4a35b 100644 --- a/server/src/leap/soledad/server/session.py +++ b/server/src/leap/soledad/server/session.py @@ -19,8 +19,9 @@ Twisted resource containing an authenticated Soledad session. """ from zope.interface import implementer +from twisted.cred.credentials import Anonymous from twisted.cred import error -from twisted.python import log +from twisted.logger import Logger from twisted.web import util from twisted.web._auth import wrapper from twisted.web.guard import HTTPAuthSessionWrapper @@ -32,6 +33,9 @@ from leap.soledad.server.auth import credentialFactory from leap.soledad.server.url_mapper import URLMapper +log = Logger() + + @implementer(IResource) class UnauthorizedResource(wrapper.UnauthorizedResource): isLeaf = True @@ -80,7 +84,7 @@ class SoledadSession(HTTPAuthSessionWrapper): # get authorization header or fail header = request.getHeader(b'authorization') if not header: - return UnauthorizedResource() + return util.DeferredResource(self._login(Anonymous())) # parse the authorization header auth_data = self._parseHeader(header) diff --git a/server/src/leap/soledad/server/url_mapper.py b/server/src/leap/soledad/server/url_mapper.py index 483f7e87..a0edeaca 100644 --- a/server/src/leap/soledad/server/url_mapper.py +++ b/server/src/leap/soledad/server/url_mapper.py @@ -53,6 +53,7 @@ class URLMapper(object): URL path | Authorized actions -------------------------------------------------- / | GET + /robots.txt | GET /shared-db | GET /shared-db/docs | - /shared-db/doc/{any_id} | GET, PUT, DELETE @@ -64,6 +65,8 @@ class URLMapper(object): """ # auth info for global resource self._connect('/', ['GET']) + # robots + self._connect('/robots.txt', ['GET']) # auth info for shared-db database resource self._connect('/%s' % SHARED_DB_NAME, ['GET']) # auth info for shared-db doc resource -- cgit v1.2.3 From 87bfc0ec7d7faae9dceea3717611a1a2851ad93f Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 15 Feb 2017 00:37:04 +0100 Subject: [feature] add robots.txt -Resolves: #6178 --- server/src/leap/soledad/server/_resource.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_resource.py b/server/src/leap/soledad/server/_resource.py index e04c0708..7a00ad9a 100644 --- a/server/src/leap/soledad/server/_resource.py +++ b/server/src/leap/soledad/server/_resource.py @@ -27,21 +27,27 @@ from ._wsgi import get_sync_resource __all__ = ['SoledadResource', 'SoledadAnonResource'] -class Robots(Resource): +class _Robots(Resource): def render_GET(self, request): - return 'robots, go away! please!' + return ( + 'User-agent: *\n' + 'Disallow: /\n' + '# you are not a robot, are you???') class SoledadAnonResource(Resource): + """ - The parts of Soledad Server that unauthenticated users can see + The parts of Soledad Server that unauthenticated users can see. + This is nice because this means that a non-authenticated user will get 404 + for anything that is not in this minimal resource tree. """ def __init__(self, enable_blobs=False): Resource.__init__(self) server_info = ServerInfo(enable_blobs) self.putChild('', server_info) - self.putChild('robots.txt', Robots()) + self.putChild('robots.txt', _Robots()) class SoledadResource(Resource): -- cgit v1.2.3 From 5c6fe9dc71d2e47f4698acf550b9fd16ce86637b Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 15 Feb 2017 00:48:40 +0100 Subject: [docs] add a to-do about perf --- server/src/leap/soledad/server/auth.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index 6ce11e71..aea003ff 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -31,8 +31,8 @@ from twisted.cred.credentials import Anonymous from twisted.cred.credentials import UsernamePassword from twisted.cred.portal import IRealm from twisted.cred.portal import Portal -from twisted.logger import Logger from twisted.internet import defer +from twisted.logger import Logger from twisted.web.iweb import ICredentialFactory from twisted.web.resource import IResource @@ -65,6 +65,11 @@ class SoledadRealm(object): return (IResource, resource, lambda: None) # Authenticated users + + # XXX review this... we're creating a Resource tree + # for each request, for every user. + # What are the perf implications of this?? + if IResource in interfaces: resource = SoledadResource( enable_blobs=enable_blobs, @@ -113,7 +118,6 @@ class TokenChecker(object): def requestAvatarId(self, credentials): if IAnonymous.providedBy(credentials): - log.warn('we are anon') return defer.succeed(Anonymous()) uuid = credentials.username @@ -125,7 +129,6 @@ class TokenChecker(object): db = self._tokens_db() token = db.get(sha512(token).hexdigest()) if token is None: - log.warn('token is none') return defer.fail(error.UnauthorizedLogin()) # TODO -- use cryptography constant time builtin comparison. -- cgit v1.2.3 From 6d7dd39fb3d4f138595f885d19315008d13f8907 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 15 Feb 2017 01:53:01 +0100 Subject: [tests] fix tests --- server/src/leap/soledad/server/auth.py | 4 ++-- server/src/leap/soledad/server/entrypoint.py | 1 - server/src/leap/soledad/server/session.py | 8 +++++--- 3 files changed, 7 insertions(+), 6 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index aea003ff..c52370cb 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -55,7 +55,6 @@ class SoledadRealm(object): self._sync_pool = sync_pool def requestAvatar(self, avatarId, mind, *interfaces): - log.warn('avatarId {0}'.format(avatarId)) enable_blobs = self._conf['blobs'] # Anonymous access @@ -66,7 +65,8 @@ class SoledadRealm(object): # Authenticated users - # XXX review this... we're creating a Resource tree + # TODO review this: #8770 ---------------- + # we're creating a Resource tree # for each request, for every user. # What are the perf implications of this?? diff --git a/server/src/leap/soledad/server/entrypoint.py b/server/src/leap/soledad/server/entrypoint.py index b10bfed6..8078a54a 100644 --- a/server/src/leap/soledad/server/entrypoint.py +++ b/server/src/leap/soledad/server/entrypoint.py @@ -35,7 +35,6 @@ conf = get_config() class SoledadEntrypoint(SoledadSession): def __init__(self): - self._credentialFactories = [] SoledadSession.__init__(self) diff --git a/server/src/leap/soledad/server/session.py b/server/src/leap/soledad/server/session.py index 70e4a35b..515fd9b3 100644 --- a/server/src/leap/soledad/server/session.py +++ b/server/src/leap/soledad/server/session.py @@ -22,6 +22,7 @@ from zope.interface import implementer from twisted.cred.credentials import Anonymous from twisted.cred import error from twisted.logger import Logger +from twisted.python import log from twisted.web import util from twisted.web._auth import wrapper from twisted.web.guard import HTTPAuthSessionWrapper @@ -33,9 +34,6 @@ from leap.soledad.server.auth import credentialFactory from leap.soledad.server.url_mapper import URLMapper -log = Logger() - - @implementer(IResource) class UnauthorizedResource(wrapper.UnauthorizedResource): isLeaf = True @@ -63,6 +61,8 @@ class SoledadSession(HTTPAuthSessionWrapper): self._mapper = URLMapper() self._portal = portal self._credentialFactory = credentialFactory + # expected by the contract of the parent class + self._credentialFactories = [credentialFactory] def _matchPath(self, request): match = self._mapper.match(request.path, request.method) @@ -97,6 +97,8 @@ class SoledadSession(HTTPAuthSessionWrapper): except error.LoginFailed: return UnauthorizedResource() except: + # If you port this to the newer log facility, be aware that + # the tests rely on the error to be logged. log.err(None, "Unexpected failure from credentials factory") return ErrorPage(500, None, None) -- cgit v1.2.3 From 13fdd28cd7448b11a35c794e69e5c64e1c9cd154 Mon Sep 17 00:00:00 2001 From: Victor Shyba Date: Wed, 15 Feb 2017 05:17:43 -0300 Subject: [style] pep8 --- server/src/leap/soledad/server/session.py | 1 - 1 file changed, 1 deletion(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/session.py b/server/src/leap/soledad/server/session.py index 515fd9b3..c1ceb340 100644 --- a/server/src/leap/soledad/server/session.py +++ b/server/src/leap/soledad/server/session.py @@ -21,7 +21,6 @@ from zope.interface import implementer from twisted.cred.credentials import Anonymous from twisted.cred import error -from twisted.logger import Logger from twisted.python import log from twisted.web import util from twisted.web._auth import wrapper -- cgit v1.2.3 From bab34cde11bdeb2810cc9f5d223957af26b2b6d3 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 17 Feb 2017 16:15:10 +0100 Subject: [refactor] create resources only once it doesn't make sense to create the resources for every request, we can reuse the same resource and create it in the constructor. - Resolves: #8770 --- server/src/leap/soledad/server/_wsgi.py | 3 +++ server/src/leap/soledad/server/auth.py | 35 ++++++++++++++------------------- 2 files changed, 18 insertions(+), 20 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_wsgi.py b/server/src/leap/soledad/server/_wsgi.py index 37a03ced..a719aacb 100644 --- a/server/src/leap/soledad/server/_wsgi.py +++ b/server/src/leap/soledad/server/_wsgi.py @@ -27,6 +27,8 @@ from leap.soledad.common.backend import SoledadBackend from leap.soledad.common.couch.state import CouchServerState from leap.soledad.common.log import getLogger +from twisted.logger import Logger +log = Logger() __all__ = ['init_couch_state', 'get_sync_resource'] @@ -62,6 +64,7 @@ def init_couch_state(conf): def get_sync_resource(pool=None): if not pool: + log.warn("NO POOL PASSED, CREATING----------") pool = threadpool.ThreadPool() reactor.callWhenRunning(pool.start) reactor.addSystemEventTrigger('after', 'shutdown', pool.stop) diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index c52370cb..e064341b 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -51,30 +51,25 @@ class SoledadRealm(object): def __init__(self, conf=None, sync_pool=None): if not conf: conf = get_config() - self._conf = conf - self._sync_pool = sync_pool + blobs = conf['blobs'] + self.anon_resource = SoledadAnonResource( + enable_blobs=blobs) + self.auth_resource = SoledadResource( + enable_blobs=blobs, + sync_pool=sync_pool) def requestAvatar(self, avatarId, mind, *interfaces): - enable_blobs = self._conf['blobs'] # Anonymous access if IAnonymous.providedBy(avatarId): - resource = SoledadAnonResource( - enable_blobs=enable_blobs) - return (IResource, resource, lambda: None) - - # Authenticated users - - # TODO review this: #8770 ---------------- - # we're creating a Resource tree - # for each request, for every user. - # What are the perf implications of this?? - - if IResource in interfaces: - resource = SoledadResource( - enable_blobs=enable_blobs, - sync_pool=self._sync_pool) - return (IResource, resource, lambda: None) + return (IResource, self.anon_resource, + lambda: None) + + # Authenticated access + else: + if IResource in interfaces: + return (IResource, self.auth_resource, + lambda: None) raise NotImplementedError() @@ -165,7 +160,7 @@ class TokenCredentialFactory(object): raise error.LoginFailed('Invalid credentials') -def get_portal(sync_pool=None): +def portalFactory(sync_pool=None): realm = SoledadRealm(sync_pool=sync_pool) checker = TokenChecker() return Portal(realm, [checker]) -- cgit v1.2.3 From 193573cf8d44a3b6a7d8ae0e43988cffb38a428a Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 17 Feb 2017 16:52:07 +0100 Subject: [bug] reuse wsgi threadpool it seems evident that the functions were thought to pass a threadpool along, but it finally wasn't properly passed and so there was a new threadpool created to handle every resource. I have removed the creation from the factory because I don't think it makes sense to create a threadpool on the fly, it's prone to errors. - Resolves: #8774 --- server/src/leap/soledad/server/_wsgi.py | 8 +------- server/src/leap/soledad/server/auth.py | 7 ++++--- server/src/leap/soledad/server/entrypoint.py | 10 ++++++++-- server/src/leap/soledad/server/session.py | 6 +----- 4 files changed, 14 insertions(+), 17 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_wsgi.py b/server/src/leap/soledad/server/_wsgi.py index a719aacb..f6ff6b26 100644 --- a/server/src/leap/soledad/server/_wsgi.py +++ b/server/src/leap/soledad/server/_wsgi.py @@ -18,7 +18,6 @@ A WSGI application that serves Soledad synchronization. """ from twisted.internet import reactor -from twisted.python import threadpool from twisted.web.wsgi import WSGIResource from leap.soledad.server import SoledadApp @@ -62,10 +61,5 @@ def init_couch_state(conf): reactor.stop() -def get_sync_resource(pool=None): - if not pool: - log.warn("NO POOL PASSED, CREATING----------") - pool = threadpool.ThreadPool() - reactor.callWhenRunning(pool.start) - reactor.addSystemEventTrigger('after', 'shutdown', pool.stop) +def get_sync_resource(pool): return WSGIResource(reactor, pool, wsgi_application) diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index e064341b..b5744fe9 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -48,8 +48,9 @@ log = Logger() @implementer(IRealm) class SoledadRealm(object): - def __init__(self, conf=None, sync_pool=None): - if not conf: + def __init__(self, sync_pool, conf=None): + assert sync_pool is not None + if conf is None: conf = get_config() blobs = conf['blobs'] self.anon_resource = SoledadAnonResource( @@ -160,7 +161,7 @@ class TokenCredentialFactory(object): raise error.LoginFailed('Invalid credentials') -def portalFactory(sync_pool=None): +def portalFactory(sync_pool): realm = SoledadRealm(sync_pool=sync_pool) checker = TokenChecker() return Portal(realm, [checker]) diff --git a/server/src/leap/soledad/server/entrypoint.py b/server/src/leap/soledad/server/entrypoint.py index 8078a54a..c06b740e 100644 --- a/server/src/leap/soledad/server/entrypoint.py +++ b/server/src/leap/soledad/server/entrypoint.py @@ -22,10 +22,12 @@ or the systemd script. """ from twisted.internet import reactor +from twisted.python import threadpool +from .auth import portalFactory +from .session import SoledadSession from ._config import get_config from ._wsgi import init_couch_state -from .session import SoledadSession # load configuration from file @@ -35,7 +37,11 @@ conf = get_config() class SoledadEntrypoint(SoledadSession): def __init__(self): - SoledadSession.__init__(self) + pool = threadpool.ThreadPool(name='wsgi') + reactor.callWhenRunning(pool.start) + reactor.addSystemEventTrigger('after', 'shutdown', pool.stop) + portal = portalFactory(pool) + SoledadSession.__init__(self, portal) # see the comments in application.py recarding why couch state has to be diff --git a/server/src/leap/soledad/server/session.py b/server/src/leap/soledad/server/session.py index c1ceb340..1c1b5345 100644 --- a/server/src/leap/soledad/server/session.py +++ b/server/src/leap/soledad/server/session.py @@ -28,7 +28,6 @@ from twisted.web.guard import HTTPAuthSessionWrapper from twisted.web.resource import ErrorPage from twisted.web.resource import IResource -from leap.soledad.server.auth import get_portal from leap.soledad.server.auth import credentialFactory from leap.soledad.server.url_mapper import URLMapper @@ -53,10 +52,7 @@ class UnauthorizedResource(wrapper.UnauthorizedResource): @implementer(IResource) class SoledadSession(HTTPAuthSessionWrapper): - def __init__(self, portal=None): - if portal is None: - portal = get_portal() - + def __init__(self, portal): self._mapper = URLMapper() self._portal = portal self._credentialFactory = credentialFactory -- cgit v1.2.3 From 7eb1ffa1d49a7e0016c5980da71151e715abc77a Mon Sep 17 00:00:00 2001 From: drebs Date: Tue, 21 Feb 2017 14:16:26 -0300 Subject: [feat] add configurable blobs path in server - Resolves: #8777 --- server/src/leap/soledad/server/_blobs.py | 10 ++++++++++ server/src/leap/soledad/server/_config.py | 1 + 2 files changed, 11 insertions(+) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_blobs.py b/server/src/leap/soledad/server/_blobs.py index 0424aae0..ae09f409 100644 --- a/server/src/leap/soledad/server/_blobs.py +++ b/server/src/leap/soledad/server/_blobs.py @@ -19,14 +19,24 @@ Blobs Server implementation. """ from twisted.web import resource +from ._config import get_config + __all__ = ['blobs_resource'] +_config = get_config() +DEFAULT_BLOBS_PATH = _config['blobs_path'] + + class BlobsResource(resource.Resource): isLeaf = True + def __init__(self, blobs_path=DEFAULT_BLOBS_PATH): + resource.Resource.__init__(self) + self._blobs_path = blobs_path + def render_GET(self, request): return 'blobs is not implemented yet!' diff --git a/server/src/leap/soledad/server/_config.py b/server/src/leap/soledad/server/_config.py index 1818c38d..e89e70d6 100644 --- a/server/src/leap/soledad/server/_config.py +++ b/server/src/leap/soledad/server/_config.py @@ -29,6 +29,7 @@ CONFIG_DEFAULTS = { 'admin_netrc': '/etc/couchdb/couchdb-admin.netrc', 'batching': True, 'blobs': False, + 'blobs_path': '/srv/leap/soledad/blobs', }, 'database-security': { 'members': ['soledad'], -- cgit v1.2.3 From 5139e95a65cf6094711ebf12aca01fb6e9b47c8c Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 27 Feb 2017 16:10:36 -0300 Subject: [style] move path config closer to blobs resource instantiation --- server/src/leap/soledad/server/_blobs.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'server/src') diff --git a/server/src/leap/soledad/server/_blobs.py b/server/src/leap/soledad/server/_blobs.py index ae09f409..cacabbdf 100644 --- a/server/src/leap/soledad/server/_blobs.py +++ b/server/src/leap/soledad/server/_blobs.py @@ -22,18 +22,14 @@ from twisted.web import resource from ._config import get_config -__all__ = ['blobs_resource'] - - -_config = get_config() -DEFAULT_BLOBS_PATH = _config['blobs_path'] +__all__ = ['BlobsResource', 'blobs_resource'] class BlobsResource(resource.Resource): isLeaf = True - def __init__(self, blobs_path=DEFAULT_BLOBS_PATH): + def __init__(self, blobs_path): resource.Resource.__init__(self) self._blobs_path = blobs_path @@ -41,4 +37,8 @@ class BlobsResource(resource.Resource): return 'blobs is not implemented yet!' -blobs_resource = BlobsResource() +# provide a configured instance of the resource +_config = get_config() +_path = _config['blobs_path'] + +blobs_resource = BlobsResource(_path) -- cgit v1.2.3