diff --git a/README.md b/README.md index 63a95e0..7afa9bd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ -### TODO before merge -- testing and tests -- need someone to test isolation of PostgreSQL users - # pyceo [![Build Status](https://ci.csclub.uwaterloo.ca/api/badges/public/pyceo/status.svg?ref=refs/heads/v1)](https://ci.csclub.uwaterloo.ca/public/pyceo) @@ -37,37 +33,37 @@ On phosphoric-acid, you will additionally need to create a principal called `ceod/admin` (remember to addprinc **and** ktadd). #### Database -Edit the `/etc/csc/ceod.ini` with the credentials required to access MySQL and PostgreSQL +create superuser `mysql` with password `mysql` ``` -[mysql] -host = -username = -password = +mysql -u root -[postgresql] -host = -usrename = -password = +CREATE USER 'mysql'@'localhost' IDENTIFIED BY 'mysql'; +GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'localhost' WITH GRANT OPTION; ``` -#### PostgreSQL Database -PostgreSQL is not designed for isolation of users and by default will allow any user to connect and edit any database. To disallow users to create public schema we run +modify superuser `postgres` for password authentication and restrict new users ``` su postgres psql +ALTER USER postgres WITH PASSWORD 'postgres'; REVOKE ALL ON SCHEMA public FROM public; GRANT ALL ON SCHEMA public TO postgres; ``` -We also want to change `pg_hba.conf` to only allow local connections and force the requested database to have the same name as the user creating the connection ([more info](https://www.postgresql.org/docs/9.1/auth-pg-hba-conf.html)) +create a new `pg_hba.conf` to force password authentication and reject non local ``` +cd /etc/postgres/// +mv pg_hba.conf pg_hba.conf.old +``` +``` +# new pg_hba.conf # TYPE DATABASE USER ADDRESS METHOD -local all postgres peer +local all postgres md5 local sameuser all md5 +host all all 0.0.0.0/0 reject +``` +``` +systemctl restart postgres ``` -- peer authentication only requires that your os username matches the postgres username (no password) -- Users will have access to list of databases and users, and this cannot be disabled without possible issues ([more info](https://wiki.postgresql.org/wiki/Shared_Database_Hosting#template1)) -- [Managing rights in PostgreSQL](https://wiki.postgresql.org/images/d/d1/Managing_rights_in_postgresql.pdf) - #### Dependencies Next, install and activate a virtualenv: diff --git a/ceo_common/interfaces/IDatabaseService.py b/ceo_common/interfaces/IDatabaseService.py index f43f1ea..fcfbd7d 100644 --- a/ceo_common/interfaces/IDatabaseService.py +++ b/ceo_common/interfaces/IDatabaseService.py @@ -6,9 +6,11 @@ class IDatabaseService(Interface): """Interface to create databases for users.""" type = Attribute('the type of databases that will be created') - host = Attribute('the database host') auth_username = Attribute('username to a privileged user on the database host') auth_password = Attribute('password to a privileged user on the database host') def create_db(username: str) -> str: """try to create a database and user and return its password""" + + def delete_db(username: str): + """remove user and delete their database""" diff --git a/ceod/api/database.py b/ceod/api/database.py index f7695a1..402a8ab 100644 --- a/ceod/api/database.py +++ b/ceod/api/database.py @@ -29,6 +29,24 @@ def create_db_from_type(db_type: str, username: str): return {'error': 'Unexpected error'}, 500 +def delete_db_from_type(db_type: str, username: str): + try: + if not username.isalnum(): # username should not contain symbols + raise UserNotFoundError + ldap_srv = component.getUtility(ILDAPService) + ldap_srv.get_user(username) # make sure user exists + db_srv = component.getUtility(IDatabaseService, db_type) + db_srv.delete_db(username) + except UserNotFoundError: + return {'error': 'user not found'}, 404 + except DatabaseConnectionError: + return {'error': 'unable to connect or authenticate to sql server'}, 400 + except DatabasePermissionError: + return {'error': 'unable to perform action due to permissions'}, 502 + except: + return {'error': 'Unexpected error'}, 500 + + @bp.route('/mysql/', methods=['POST']) @requires_authentication_no_realm def create_mysql_db(auth_user: str, username: str): @@ -37,9 +55,23 @@ def create_mysql_db(auth_user: str, username: str): return create_db_from_type('mysql', username) +@bp.route('/mysql/', methods=['DELETE']) +@authz_restrict_to_syscom +@development_only +def delete_mysql_db(username: str): + delete_db_from_type('mysql', username) + + @bp.route('/postgresql/', methods=['POST']) @requires_authentication_no_realm def create_postgresql_db(auth_user: str, username: str): if not (auth_user == username or user_is_in_group(auth_user, 'syscom')): return {'error': "not authorized to create databases for others"}, 403 return create_db_from_type('postgresql', username) + + +@bp.route('/postgresql/', methods=['DELETE']) +@authz_restrict_to_syscom +@development_only +def delete_postgresql_db(username: str): + delete_db_from_type('postgresl', username) diff --git a/ceod/db/MySQLService.py b/ceod/db/MySQLService.py index 9cc9ee5..875c635 100644 --- a/ceod/db/MySQLService.py +++ b/ceod/db/MySQLService.py @@ -13,7 +13,6 @@ class MySQLService: def __init__(self): self.type = 'mysql' config = component.getUtility(IConfig) - self.host = config.get('mysql_host') self.auth_username = config.get('mysql_username') self.auth_password = config.get('mysql_password') @@ -22,27 +21,43 @@ class MySQLService: search_for_user = f"SELECT user FROM mysql.user WHERE user='{username}'" search_for_db = f"SHOW DATABASES LIKE '{username}'" create_user = f"CREATE USER '{username}'@'localhost' IDENTIFIED BY %(password)s" - reset_password = f"ALTER USER '{username}' IDENTIFIED BY %(password)s" + reset_password = f"ALTER USER '{username}'@'localhost' IDENTIFIED BY %(password)s" create_database = f""" CREATE DATABASE {username}; GRANT ALL PRIVILEGES ON {username}.* TO '{username}'@'localhost'; """ try: with connect( - host=self.host, + host='localhost', user=self.auth_username, password=self.auth_password, ) as con: with con.cursor() as cursor: if response_is_empty(search_for_user, con): - cursor.execute(create_user, {password: password}) + cursor.execute(create_user, {'password': password}) else: - cursor.execute(reset_password, {password: password}) + cursor.execute(reset_password, {'password': password}) if response_is_empty(search_for_db, con): cursor.execute(create_database) - con.commit() return password except InterfaceError: raise DatabaseConnectionError() except ProgrammingError: raise DatabasePermissionError() + + def delete_db(self, username: str): + drop_user = f"DROP USER IF EXISTS '{username}'@'localhost'" + drop_db = f"DROP DATABASE IF EXISTS {username}" + try: + with connect( + host='localhost', + user=self.auth_username, + password=self.auth_password, + ) as con: + with con.cursor() as cursor: + cursor.execute(drop_user) + cursor.execute(drop_db) + except InterfaceError: + raise DatabaseConnectionError() + except ProgrammingError: + raise DatabasePermissionError() diff --git a/ceod/db/PostgreSQLService.py b/ceod/db/PostgreSQLService.py index ba0ba64..15e9e0b 100644 --- a/ceod/db/PostgreSQLService.py +++ b/ceod/db/PostgreSQLService.py @@ -13,7 +13,6 @@ class PostgreSQLService: def __init__(self): self.type = 'postgresql' config = component.getUtility(IConfig) - self.host = config.get('postgresql_host') self.auth_username = config.get('postgresql_username') self.auth_password = config.get('postgresql_password') @@ -29,16 +28,16 @@ class PostgreSQLService: """ try: with connect( - host=self.host, - user=self.auth_username, - password=self.auth_password, + host='localhost', + user=self.auth_username, + password=self.auth_password, ) as con: con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) with con.cursor() as cursor: if response_is_empty(search_for_user, con): - cursor.execute(create_user, {password: password}) + cursor.execute(create_user, {'password': password}) else: - cursor.execute(reset_password, {password: password}) + cursor.execute(reset_password, {'password': password}) if response_is_empty(search_for_db, con): cursor.execute(create_database) return password @@ -47,3 +46,22 @@ class PostgreSQLService: except ProgrammingError: raise DatabasePermissionError() + def delete_db(self, username: str): + drop_user = f"DROP USER IF EXISTS {username}" + drop_db = f"DROP DATABASE IF EXISTS {username}" + try: + with connect( + host='localhost', + user=self.auth_username, + password=self.auth_password, + ) as con: + con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + with con.cursor() as cursor: + cursor.execute(drop_user) + cursor.execute(drop_db) + except OperationalError: + raise DatabaseConnectionError() + except ProgrammingError: + raise DatabasePermissionError() + + diff --git a/tests/ceod/db/__init__.py b/tests/ceod/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ceod/db/test_mysql.py b/tests/ceod/db/test_mysql.py new file mode 100644 index 0000000..ce1c76f --- /dev/null +++ b/tests/ceod/db/test_mysql.py @@ -0,0 +1,11 @@ +import pytest + +# ask for mysql and postgres with proper postgres configs and no public schema + +# tests are stateless +# each test should not require anything before or change anything +# this means you should delete user and databases created after done + +# attempt to create database +# password reset +# recreate database (user dropped database) diff --git a/tests/ceod/db/test_postgres.py b/tests/ceod/db/test_postgres.py new file mode 100644 index 0000000..a1ace31 --- /dev/null +++ b/tests/ceod/db/test_postgres.py @@ -0,0 +1,2 @@ +import pytest + diff --git a/tests/ceod_dev.ini b/tests/ceod_dev.ini index 6e7fd38..f18e2a8 100644 --- a/tests/ceod_dev.ini +++ b/tests/ceod_dev.ini @@ -52,3 +52,11 @@ office = cdrom,audio,video,www [auxiliary mailing lists] syscom = syscom,syscom-alerts exec = exec + +[mysql] +username = mysql +password = mysql + +[postgresql] +username = postgres +password = postgres diff --git a/tests/ceod_test_local.ini b/tests/ceod_test_local.ini index f14419e..c9b7325 100644 --- a/tests/ceod_test_local.ini +++ b/tests/ceod_test_local.ini @@ -49,3 +49,11 @@ syscom = office,staff [auxiliary mailing lists] syscom = syscom,syscom-alerts exec = exec + +[mysql] +username = mysql +password = mysql + +[postgresql] +username = postgres +password = postgres