From 6f505d90ee4f9298a608119ef5d007e3e26e80f6 Mon Sep 17 00:00:00 2001 From: Dominik Moritz Date: Mon, 10 Sep 2012 19:09:13 +0100 Subject: [PATCH] paster command to create database and read-only user --- ...create_datastore_db_and_read_only_user.sql | 2 +- ckanext/datastore/commands.py | 113 ++++++++++++++++++ ckanext/datastore/plugin.py | 9 +- setup.py | 1 + 4 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 ckanext/datastore/commands.py diff --git a/ckanext/datastore/bin/create_datastore_db_and_read_only_user.sql b/ckanext/datastore/bin/create_datastore_db_and_read_only_user.sql index 6522fc7e8a6..ae6f0a38a9c 100644 --- a/ckanext/datastore/bin/create_datastore_db_and_read_only_user.sql +++ b/ckanext/datastore/bin/create_datastore_db_and_read_only_user.sql @@ -40,7 +40,7 @@ GRANT USAGE ON SCHEMA public TO :ckanuser; -- create new read only user CREATE USER :rouser NOSUPERUSER NOCREATEDB NOCREATEROLE LOGIN; --- take connect permissions to main db +-- take connect permissions from main db REVOKE CONNECT ON DATABASE :maindb FROM :rouser; -- grant select permissions for read-only user diff --git a/ckanext/datastore/commands.py b/ckanext/datastore/commands.py new file mode 100644 index 00000000000..e3a203301f7 --- /dev/null +++ b/ckanext/datastore/commands.py @@ -0,0 +1,113 @@ +import re +from ckan.lib.cli import CkanCommand + +import logging +log = logging.getLogger() + +read_only_user_sql = ''' +-- revoke permissions for the new user +REVOKE CREATE ON SCHEMA public FROM PUBLIC; +REVOKE USAGE ON SCHEMA public FROM PUBLIC; + +GRANT CREATE ON SCHEMA public TO "{ckanuser}"; +GRANT USAGE ON SCHEMA public TO "{ckanuser}"; + +GRANT CREATE ON SCHEMA public TO "{writeuser}"; +GRANT USAGE ON SCHEMA public TO "{writeuser}"; + +-- create new read only user +CREATE USER "{readonlyuser}" NOSUPERUSER NOCREATEDB NOCREATEROLE LOGIN; + +-- take connect permissions from main db +REVOKE CONNECT ON DATABASE "{maindb}" FROM "{readonlyuser}"; + +-- grant select permissions for read-only user +GRANT CONNECT ON DATABASE "{datastore}" TO "{readonlyuser}"; +GRANT USAGE ON SCHEMA public TO "{readonlyuser}"; + +-- grant access to current tables and views +GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{readonlyuser}"; + +-- grant access to new tables and views by default +ALTER DEFAULT PRIVILEGES FOR USER "{ckanuser}" IN SCHEMA public + GRANT SELECT ON TABLES TO "{readonlyuser}"; +''' + +class SetupDatastoreCommand(CkanCommand): + '''Perform commands to set up the datastore. + Make sure that the datastore urls are set properly before you run these commands. + + Usage:: + + create-db - create the datastore database + create-read-only-user - create a read-only user for the datastore read url + ''' + summary = __doc__.split('\n')[0] + usage = __doc__ + + def __init__(self,name): + + super(SetupDatastoreCommand,self).__init__(name) + + def command(self): + ''' + Parse command line arguments and call appropriate method. + ''' + if not self.args or self.args[0] in ['--help', '-h', 'help']: + print SetupDatastoreCommand.__doc__ + return + + cmd = self.args[0] + self._load_config() + + self.urlparts_w = self._get_db_config('ckan.datastore_write_url') + self.urlparts_r = self._get_db_config('ckan.datastore_read_url') + self.urlparts_c = self._get_db_config('sqlalchemy.url') + + assert self.urlparts_w['db_name'] == self.urlparts_r['db_name'], "write and read db should be the same" + + if cmd == 'create-db': + self.create_db() + if self.verbose: + print 'Initialising DB: SUCCESS' + elif cmd == 'create-read-only-user': + self.create_read_only_user() + if self.verbose: + print 'Initialising read-only user: SUCCESS' + else: + log.error('Command "%s" not recognized' % (cmd,)) + + def _get_db_config(self, name): + from pylons import config + url = config[name] + # e.g. 'postgres://tester:pass@localhost/ckantest3' + db_details_match = re.match('^\s*(?P\w*)://(?P[^:]*):?(?P[^@]*)@(?P[^/:]*):?(?P[^/]*)/(?P[\w.-]*)', url) + if not db_details_match: + raise Exception('Could not extract db details from url: %r' % url) + db_details = db_details_match.groupdict() + return db_details + + def _run_cmd(self, command_line): + import subprocess + retcode = subprocess.call(command_line, shell=True) + if retcode != 0: + raise SystemError('Command exited with errorcode: %i' % retcode) + + def _run_sql(self, sql, database='postgres'): + if self.verbose: + print "Executing: \n#####\n", sql, "\n####\nOn database:", database + self._run_cmd("sudo -u postgres psql {database} -c '{sql}'".format(sql=sql, database=database)) + + def create_db(self): + sql = "create database {0}".format(self.urlparts_w['db_name']) + self._run_sql(sql) + + def create_read_only_user(self): + sql = read_only_user_sql.format( + maindb=self.urlparts_c['db_name'], + datastore=self.urlparts_w['db_name'], + ckanuser=self.urlparts_c['db_user'], + readonlyuser=self.urlparts_r['db_user'], + writeuser=self.urlparts_w['db_user']) + self._run_sql(sql, self.urlparts_w['db_name']) + diff --git a/ckanext/datastore/plugin.py b/ckanext/datastore/plugin.py index d768f5eb681..c36305af040 100644 --- a/ckanext/datastore/plugin.py +++ b/ckanext/datastore/plugin.py @@ -96,15 +96,16 @@ def _check_read_permissions(self): ''' write_connection = db._get_engine(None, {'connection_url': self.write_url}).connect() - write_connection.execute(u"CREATE TABLE public.foo (id INTEGER NOT NULL, name VARCHAR)") + write_connection.execute(u"DROP TABLE IF EXISTS public._foo;" + u"CREATE TABLE public._foo (id INTEGER NOT NULL, name VARCHAR)") read_connection = db._get_engine(None, {'connection_url': self.read_url}).connect() read_trans = read_connection.begin() statements = [ - u"CREATE TABLE public.bar (id INTEGER NOT NULL, name VARCHAR)", - u"INSERT INTO public.foo VALUES (1, 'okfn')" + u"CREATE TABLE public._bar (id INTEGER NOT NULL, name VARCHAR)", + u"INSERT INTO public._foo VALUES (1, 'okfn')" ] try: @@ -124,7 +125,7 @@ def _check_read_permissions(self): except Exception: raise finally: - write_connection.execute("DROP TABLE foo") + write_connection.execute("DROP TABLE _foo") def _create_alias_table(self): mapping_sql = ''' diff --git a/setup.py b/setup.py index 02ee9ea1142..53494b7f64a 100644 --- a/setup.py +++ b/setup.py @@ -80,6 +80,7 @@ plugin-info = ckan.lib.cli:PluginInfo profile = ckan.lib.cli:Profile check-po-files = ckan.i18n.check_po_files:CheckPoFiles + datastore = ckanext.datastore.commands:SetupDatastoreCommand [console_scripts] ckan-admin = bin.ckan_admin:Command