add delete_db for testing
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Andrew Wang 2021-08-24 22:31:50 -04:00
parent af87d6a7e6
commit 2b41423a88
10 changed files with 126 additions and 34 deletions

View File

@ -1,7 +1,3 @@
### TODO before merge
- testing and tests
- need someone to test isolation of PostgreSQL users
# pyceo # 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) [![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). called `ceod/admin` (remember to addprinc **and** ktadd).
#### Database #### Database
Edit the `/etc/csc/ceod.ini` with the credentials required to access MySQL and PostgreSQL create superuser `mysql` with password `mysql`
``` ```
[mysql] mysql -u root
host =
username =
password =
[postgresql] CREATE USER 'mysql'@'localhost' IDENTIFIED BY 'mysql';
host = GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'localhost' WITH GRANT OPTION;
usrename =
password =
``` ```
#### PostgreSQL Database modify superuser `postgres` for password authentication and restrict new users
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
``` ```
su postgres su postgres
psql psql
ALTER USER postgres WITH PASSWORD 'postgres';
REVOKE ALL ON SCHEMA public FROM public; REVOKE ALL ON SCHEMA public FROM public;
GRANT ALL ON SCHEMA public TO postgres; 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/<version>/<branch>/
mv pg_hba.conf pg_hba.conf.old
```
```
# new pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD # TYPE DATABASE USER ADDRESS METHOD
local all postgres peer local all postgres md5
local sameuser all 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 #### Dependencies
Next, install and activate a virtualenv: Next, install and activate a virtualenv:

View File

@ -6,9 +6,11 @@ class IDatabaseService(Interface):
"""Interface to create databases for users.""" """Interface to create databases for users."""
type = Attribute('the type of databases that will be created') 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_username = Attribute('username to a privileged user on the database host')
auth_password = Attribute('password 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: def create_db(username: str) -> str:
"""try to create a database and user and return its password""" """try to create a database and user and return its password"""
def delete_db(username: str):
"""remove user and delete their database"""

View File

@ -29,6 +29,24 @@ def create_db_from_type(db_type: str, username: str):
return {'error': 'Unexpected error'}, 500 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/<username>', methods=['POST']) @bp.route('/mysql/<username>', methods=['POST'])
@requires_authentication_no_realm @requires_authentication_no_realm
def create_mysql_db(auth_user: str, username: str): 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) return create_db_from_type('mysql', username)
@bp.route('/mysql/<username>', methods=['DELETE'])
@authz_restrict_to_syscom
@development_only
def delete_mysql_db(username: str):
delete_db_from_type('mysql', username)
@bp.route('/postgresql/<username>', methods=['POST']) @bp.route('/postgresql/<username>', methods=['POST'])
@requires_authentication_no_realm @requires_authentication_no_realm
def create_postgresql_db(auth_user: str, username: str): def create_postgresql_db(auth_user: str, username: str):
if not (auth_user == username or user_is_in_group(auth_user, 'syscom')): if not (auth_user == username or user_is_in_group(auth_user, 'syscom')):
return {'error': "not authorized to create databases for others"}, 403 return {'error': "not authorized to create databases for others"}, 403
return create_db_from_type('postgresql', username) return create_db_from_type('postgresql', username)
@bp.route('/postgresql/<username>', methods=['DELETE'])
@authz_restrict_to_syscom
@development_only
def delete_postgresql_db(username: str):
delete_db_from_type('postgresl', username)

View File

@ -13,7 +13,6 @@ class MySQLService:
def __init__(self): def __init__(self):
self.type = 'mysql' self.type = 'mysql'
config = component.getUtility(IConfig) config = component.getUtility(IConfig)
self.host = config.get('mysql_host')
self.auth_username = config.get('mysql_username') self.auth_username = config.get('mysql_username')
self.auth_password = config.get('mysql_password') 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_user = f"SELECT user FROM mysql.user WHERE user='{username}'"
search_for_db = f"SHOW DATABASES LIKE '{username}'" search_for_db = f"SHOW DATABASES LIKE '{username}'"
create_user = f"CREATE USER '{username}'@'localhost' IDENTIFIED BY %(password)s" 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 = f"""
CREATE DATABASE {username}; CREATE DATABASE {username};
GRANT ALL PRIVILEGES ON {username}.* TO '{username}'@'localhost'; GRANT ALL PRIVILEGES ON {username}.* TO '{username}'@'localhost';
""" """
try: try:
with connect( with connect(
host=self.host, host='localhost',
user=self.auth_username, user=self.auth_username,
password=self.auth_password, password=self.auth_password,
) as con: ) as con:
with con.cursor() as cursor: with con.cursor() as cursor:
if response_is_empty(search_for_user, con): if response_is_empty(search_for_user, con):
cursor.execute(create_user, {password: password}) cursor.execute(create_user, {'password': password})
else: else:
cursor.execute(reset_password, {password: password}) cursor.execute(reset_password, {'password': password})
if response_is_empty(search_for_db, con): if response_is_empty(search_for_db, con):
cursor.execute(create_database) cursor.execute(create_database)
con.commit()
return password return password
except InterfaceError: except InterfaceError:
raise DatabaseConnectionError() raise DatabaseConnectionError()
except ProgrammingError: except ProgrammingError:
raise DatabasePermissionError() 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()

View File

@ -13,7 +13,6 @@ class PostgreSQLService:
def __init__(self): def __init__(self):
self.type = 'postgresql' self.type = 'postgresql'
config = component.getUtility(IConfig) config = component.getUtility(IConfig)
self.host = config.get('postgresql_host')
self.auth_username = config.get('postgresql_username') self.auth_username = config.get('postgresql_username')
self.auth_password = config.get('postgresql_password') self.auth_password = config.get('postgresql_password')
@ -29,16 +28,16 @@ class PostgreSQLService:
""" """
try: try:
with connect( with connect(
host=self.host, host='localhost',
user=self.auth_username, user=self.auth_username,
password=self.auth_password, password=self.auth_password,
) as con: ) as con:
con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
with con.cursor() as cursor: with con.cursor() as cursor:
if response_is_empty(search_for_user, con): if response_is_empty(search_for_user, con):
cursor.execute(create_user, {password: password}) cursor.execute(create_user, {'password': password})
else: else:
cursor.execute(reset_password, {password: password}) cursor.execute(reset_password, {'password': password})
if response_is_empty(search_for_db, con): if response_is_empty(search_for_db, con):
cursor.execute(create_database) cursor.execute(create_database)
return password return password
@ -47,3 +46,22 @@ class PostgreSQLService:
except ProgrammingError: except ProgrammingError:
raise DatabasePermissionError() 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()

View File

View File

@ -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)

View File

@ -0,0 +1,2 @@
import pytest

View File

@ -52,3 +52,11 @@ office = cdrom,audio,video,www
[auxiliary mailing lists] [auxiliary mailing lists]
syscom = syscom,syscom-alerts syscom = syscom,syscom-alerts
exec = exec exec = exec
[mysql]
username = mysql
password = mysql
[postgresql]
username = postgres
password = postgres

View File

@ -49,3 +49,11 @@ syscom = office,staff
[auxiliary mailing lists] [auxiliary mailing lists]
syscom = syscom,syscom-alerts syscom = syscom,syscom-alerts
exec = exec exec = exec
[mysql]
username = mysql
password = mysql
[postgresql]
username = postgres
password = postgres