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
[![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/<version>/<branch>/
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:

View File

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

View File

@ -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/<username>', 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/<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'])
@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/<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):
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()

View File

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

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]
syscom = syscom,syscom-alerts
exec = exec
[mysql]
username = mysql
password = mysql
[postgresql]
username = postgres
password = postgres

View File

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