diff options
author | Victor Shyba <victor.shyba@gmail.com> | 2015-10-26 18:50:20 -0300 |
---|---|---|
committer | Victor Shyba <victor.shyba@gmail.com> | 2015-10-28 18:13:40 -0300 |
commit | f8d38125098829fe50199725545365d6d2a889a6 (patch) | |
tree | 9edab8dc323606e675a31eb141d1d42ca1e72c99 | |
parent | 55548cf947966bcbb9a496e523a3f802b0f0b55f (diff) |
[feat] read security doc from configuration
LEAP Platform needs to granularly allow access on user database for
other services, like mx. This is now possible by editing
soledad-server.conf file. A new section 'database-security' was added
and it is parsed during 'create-user-db' to be set on security design
document, present on every per-user database.
-rw-r--r-- | common/src/leap/soledad/common/couch.py | 23 | ||||
-rw-r--r-- | common/src/leap/soledad/common/tests/fixture_soledad.conf | 10 | ||||
-rw-r--r-- | common/src/leap/soledad/common/tests/test_couch.py | 24 | ||||
-rw-r--r-- | common/src/leap/soledad/common/tests/test_server.py | 36 | ||||
-rwxr-xr-x | server/pkg/create-user-db | 7 | ||||
-rw-r--r-- | server/src/leap/soledad/server/__init__.py | 34 |
6 files changed, 115 insertions, 19 deletions
diff --git a/common/src/leap/soledad/common/couch.py b/common/src/leap/soledad/common/couch.py index 3dee1473..a801a044 100644 --- a/common/src/leap/soledad/common/couch.py +++ b/common/src/leap/soledad/common/couch.py @@ -372,7 +372,8 @@ class CouchDatabase(CommonBackend): """ @classmethod - def open_database(cls, url, create, replica_uid=None, ensure_ddocs=False): + def open_database(cls, url, create, replica_uid=None, ensure_ddocs=False, + database_security=None): """ Open a U1DB database using CouchDB as backend. @@ -402,9 +403,11 @@ class CouchDatabase(CommonBackend): raise DatabaseDoesNotExist() server.create(dbname) return cls( - url, dbname, replica_uid=replica_uid, ensure_ddocs=ensure_ddocs) + url, dbname, replica_uid=replica_uid, + ensure_ddocs=ensure_ddocs, database_security=database_security) - def __init__(self, url, dbname, replica_uid=None, ensure_ddocs=False): + def __init__(self, url, dbname, replica_uid=None, ensure_ddocs=False, + database_security=None): """ Create a new Couch data container. @@ -435,7 +438,7 @@ class CouchDatabase(CommonBackend): self._set_replica_uid(replica_uid) if ensure_ddocs: self.ensure_ddocs_on_db() - self.ensure_security_ddoc() + self.ensure_security_ddoc(database_security) self._cache = None @property @@ -469,7 +472,7 @@ class CouchDatabase(CommonBackend): getattr(ddocs, ddoc_name))) self._database.save(ddoc) - def ensure_security_ddoc(self): + def ensure_security_ddoc(self, security_config=None): """ Make sure that only soledad user is able to access this database as an unprivileged member, meaning that administration access will @@ -478,10 +481,18 @@ class CouchDatabase(CommonBackend): to the unprivileged CouchDB user set on the server process. This is achieved by creating a _security design document, see: http://docs.couchdb.org/en/latest/api/database/security.html + + :param database_security: security configuration parsed from conf file + :type cache: dict """ + security_config = security_config or {} security = self._database.resource.get_json('_security')[2] - security['members'] = {'names': ['soledad'], 'roles': []} + security['members'] = {'names': [], 'roles': []} + security['members']['names'] = security_config.get('members', ['soledad']) + security['members']['roles'] = security_config.get('members_roles', []) security['admins'] = {'names': [], 'roles': []} + security['admins']['names'] = security_config.get('admins', []) + security['admins']['roles'] = security_config.get('admins_roles', []) self._database.resource.put_json('_security', body=security) def get_sync_target(self): diff --git a/common/src/leap/soledad/common/tests/fixture_soledad.conf b/common/src/leap/soledad/common/tests/fixture_soledad.conf new file mode 100644 index 00000000..c0ffacf6 --- /dev/null +++ b/common/src/leap/soledad/common/tests/fixture_soledad.conf @@ -0,0 +1,10 @@ +[soledad-server] +couch_url = http://soledad:passwd@localhost:5984 +create_cmd = sudo -u soledad-admin /usr/bin/create-user-db +admin_netrc = /etc/couchdb/couchdb-soledad-admin.netrc + +[database-security] +members = user1, user2 +members_roles = role1, role2 +admins = user3, user4 +admins_roles = role3, role3 diff --git a/common/src/leap/soledad/common/tests/test_couch.py b/common/src/leap/soledad/common/tests/test_couch.py index b4797f5e..668a7f64 100644 --- a/common/src/leap/soledad/common/tests/test_couch.py +++ b/common/src/leap/soledad/common/tests/test_couch.py @@ -534,9 +534,11 @@ class CouchDatabaseSyncTargetTests( class IndexedCouchDatabase(couch.CouchDatabase): - def __init__(self, url, dbname, replica_uid=None, ensure_ddocs=True): + def __init__(self, url, dbname, replica_uid=None, ensure_ddocs=True, + database_security=None): old_class.__init__(self, url, dbname, replica_uid=replica_uid, - ensure_ddocs=ensure_ddocs) + ensure_ddocs=ensure_ddocs, + database_security=database_security) self._indexes = {} def _put_doc(self, old_doc, doc): @@ -1515,6 +1517,24 @@ class CouchDatabaseExceptionsTests(CouchDBTestCase): self.assertIn('members', security_ddoc) self.assertIn('soledad', security_ddoc['members']['names']) + def test_ensure_security_from_configuration(self): + """ + Given a configuration, follow it to create the security document + """ + self.create_db(ensure=False) + configuration = {'members': ['user1', 'user2'], + 'members_roles': ['role1', 'role2'], + 'admins': ['admin'], + 'admins_roles': ['administrators'] + } + self.db.ensure_security_ddoc(configuration) + + security_ddoc = self.db._database.resource.get_json('_security')[2] + self.assertEquals(configuration['admins'], security_ddoc['admins']['names']) + self.assertEquals(configuration['admins_roles'], security_ddoc['admins']['roles']) + self.assertEquals(configuration['members'], security_ddoc['members']['names']) + self.assertEquals(configuration['members_roles'], security_ddoc['members']['roles']) + class DatabaseNameValidationTest(unittest.TestCase): diff --git a/common/src/leap/soledad/common/tests/test_server.py b/common/src/leap/soledad/common/tests/test_server.py index 19d2907d..14e17e67 100644 --- a/common/src/leap/soledad/common/tests/test_server.py +++ b/common/src/leap/soledad/common/tests/test_server.py @@ -22,10 +22,12 @@ import tempfile import mock import time import binascii +import pkg_resources from uuid import uuid4 from urlparse import urljoin from twisted.internet import defer +from twisted.trial import unittest from leap.soledad.common.couch import ( CouchServerState, @@ -43,6 +45,8 @@ from leap.soledad.common.tests.util import ( from leap.soledad.common import crypto from leap.soledad.client import Soledad from leap.soledad.server import LockResource +from leap.soledad.server import load_configuration +from leap.soledad.server import CONFIG_DEFAULTS from leap.soledad.server.auth import URLToAuthorization @@ -587,3 +591,35 @@ class LockResourceTestCase( self.assertIsNotNone(lr._shared_db.get_doc('lock-' + lock_uuid)) responder.send_response_json.assert_called_with( 401, error='unlock unauthorized') + + +class ConfigurationParsingTest(unittest.TestCase): + + def setUp(self): + self.maxDiff = None + + def test_use_defaults_on_failure(self): + config = load_configuration('this file will never exist') + expected = CONFIG_DEFAULTS + self.assertEquals(expected, config) + + def test_security_values_configuration(self): + # given + config_path = pkg_resources.resource_filename('leap.soledad.common.tests', + 'fixture_soledad.conf') + # when + config = load_configuration(config_path) + + # then + expected = {'soledad-server': { + 'couch_url': 'http://soledad:passwd@localhost:5984', + 'create_cmd': 'sudo -u soledad-admin /usr/bin/create-user-db', + 'admin_netrc': '/etc/couchdb/couchdb-soledad-admin.netrc', + }, + 'database-security': { + 'members': ['user1', 'user2'], + 'members_roles': ['role1', 'role2'], + 'admins': ['user3', 'user4'], + 'admins_roles': ['role3', 'role3'], + }} + self.assertDictEqual(expected, config) diff --git a/server/pkg/create-user-db b/server/pkg/create-user-db index 7eafc945..28d1cbd0 100755 --- a/server/pkg/create-user-db +++ b/server/pkg/create-user-db @@ -31,7 +31,8 @@ This is meant to be used by Soledad Server. parser = argparse.ArgumentParser(description=description) parser.add_argument('dbname', metavar='user-d34db33f', type=str, help='database name on the format user-{uuid4}') -NETRC_PATH = load_configuration('/etc/soledad/soledad-server.conf')['admin_netrc'] +CONF = load_configuration('/etc/soledad/soledad-server.conf') +NETRC_PATH = CONF['soledad-server']['admin_netrc'] def url_for_db(dbname): @@ -54,7 +55,9 @@ if __name__ == '__main__': print ("Invalid name! %s" % args.dbname) sys.exit(1) url = url_for_db(args.dbname) + db_security = CONF['database-security'] db = CouchDatabase.open_database(url=url, create=True, - replica_uid=None, ensure_ddocs=True) + replica_uid=None, ensure_ddocs=True, + database_security=db_security) print ('success! Created %s, replica_uid: %s' % (db._dbname, db.replica_uid)) diff --git a/server/src/leap/soledad/server/__init__.py b/server/src/leap/soledad/server/__init__.py index f64d07bf..4d03c82a 100644 --- a/server/src/leap/soledad/server/__init__.py +++ b/server/src/leap/soledad/server/__init__.py @@ -272,6 +272,20 @@ http_app.HTTPInvocationByMethodWithBody = HTTPInvocationByMethodWithBody # ---------------------------------------------------------------------------- # Auxiliary functions # ---------------------------------------------------------------------------- +CONFIG_DEFAULTS = { + 'soledad-server': { + 'couch_url': 'http://localhost:5984', + 'create_cmd': None, + 'admin_netrc': '/etc/couchdb/couchdb-admin.netrc', + }, + 'database-security': { + 'members': ['soledad'], + 'members_roles': [], + 'admins': [], + 'admins_roles': [] + } +} + def load_configuration(file_path): """ @@ -283,17 +297,18 @@ def load_configuration(file_path): @return: A dictionary with the configuration. @rtype: dict """ - defaults = { - 'couch_url': 'http://localhost:5984', - 'create_cmd': None, - 'admin_netrc': '/etc/couchdb/couchdb-admin.netrc', - } + defaults = dict(CONFIG_DEFAULTS) config = configparser.ConfigParser() config.read(file_path) - if 'soledad-server' in config: - for key in defaults: - if key in config['soledad-server']: - defaults[key] = config['soledad-server'][key] + for section in defaults.keys(): + if section in config: + for key in defaults[section]: + if key in config[section]: + defaults[section][key] = config[section][key] + for key, value in defaults['database-security'].iteritems(): + if type(value) is not unicode: continue + defaults['database-security'][key] = \ + [item.strip() for item in value.split(',')] # TODO: implement basic parsing/sanitization of options comming from # config file. return defaults @@ -305,6 +320,7 @@ def load_configuration(file_path): def application(environ, start_response): conf = load_configuration('/etc/soledad/soledad-server.conf') + conf = conf['soledad-server'] state = CouchServerState(conf['couch_url'], create_cmd=conf['create_cmd']) # WSGI application that may be used by `twistd -web` application = GzipMiddleware( |