add delete_db for testing
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
af87d6a7e6
commit
2b41423a88
38
README.md
38
README.md
|
@ -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:
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -0,0 +1,2 @@
|
||||||
|
import pytest
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue