add pwreset endpoint

pull/10/head
Andrew Wang 1 year ago
parent 6421a93459
commit ef3d130f78
  1. 2
      .drone.yml
  2. 5
      ceo_common/interfaces/IDatabaseService.py
  3. 88
      ceod/api/database.py
  4. 76
      ceod/db/MySQLService.py
  5. 79
      ceod/db/PostgreSQLService.py
  6. 20
      tests/ceod/db/test_mysql.py

@ -10,7 +10,7 @@ steps:
# way to share system packages between steps
commands:
# install dependencies
- apt update && apt install -y libkrb5-dev python3-dev
- apt update && apt install -y libkrb5-dev libpq-dev python3-dev
- python3 -m venv venv
- . venv/bin/activate
- pip install -r dev-requirements.txt

@ -9,7 +9,10 @@ class IDatabaseService(Interface):
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"""
"""create a user and database and return the password"""
def reset_passwd(username: str) -> str:
"""reset user password and return it"""
def delete_db(username: str):
"""remove user and delete their database"""

@ -1,50 +1,58 @@
from flask import Blueprint, request
from zope import component
from functools import wraps
from ceod.api.utils import authz_restrict_to_staff, authz_restrict_to_syscom, \
user_is_in_group, requires_authentication_no_realm, \
create_streaming_response, development_only
from ceo_common.errors import UserNotFoundError, DatabaseConnectionError, DatabasePermissionError, InvalidUsernameError
from ceo_common.errors import UserNotFoundError, DatabaseConnectionError, DatabasePermissionError, \
InvalidUsernameError, UserAlreadyExistsError
from ceo_common.interfaces import ILDAPService, IDatabaseService
bp = Blueprint('db', __name__)
def db_exception_handler(func):
@wraps(func)
def function(db_type: str, username: str):
try:
if not username.isalnum(): # username should not contain symbols
raise InvalidUsernameError()
ldap_srv = component.getUtility(ILDAPService)
ldap_srv.get_user(username) # make sure user exists
return func(db_type, username)
except UserNotFoundError:
return {'error': 'user not found'}, 404
except UserAlreadyExistsError:
return {'error': 'database user is already created'}, 409
except InvalidUsernameError:
return {'error': 'username contains invalid characters'}, 400
except DatabaseConnectionError:
return {'error': 'unable to connect or authenticate to sql server'}, 500
except DatabasePermissionError:
return {'error': 'unable to perform action due to permissions'}, 500
return function
@db_exception_handler
def create_db_from_type(db_type: str, username: str):
try:
if not username.isalnum(): # username should not contain symbols
raise InvalidUsernameError()
ldap_srv = component.getUtility(ILDAPService)
ldap_srv.get_user(username) # make sure user exists
db_srv = component.getUtility(IDatabaseService, db_type)
password = db_srv.create_db(username)
return {'password': password}
except UserNotFoundError:
return {'error': 'user not found'}, 404
except InvalidUsernameError:
return {'error': 'username contains invalid characters'}, 400
except DatabaseConnectionError:
return {'error': 'unable to connect or authenticate to sql server'}, 500
except DatabasePermissionError:
return {'error': 'unable to perform action due to permissions'}, 500
db_srv = component.getUtility(IDatabaseService, db_type)
password = db_srv.create_db(username)
return {'password': password}
@db_exception_handler
def reset_db_passwd_from_type(db_type: str, username: str):
db_srv = component.getUtility(IDatabaseService, db_type)
password = db_srv.reset_passwd(username)
return {'password': password}
@db_exception_handler
def delete_db_from_type(db_type: str, username: str):
try:
if not username.isalnum(): # username should not contain symbols
raise InvalidUsernameError()
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 InvalidUsernameError:
return {'error': 'username contains invalid characters'}, 400
except DatabaseConnectionError:
return {'error': 'unable to connect or authenticate to sql server'}, 500
except DatabasePermissionError:
return {'error': 'unable to perform action due to permissions'}, 500
db_srv = component.getUtility(IDatabaseService, db_type)
db_srv.delete_db(username)
@bp.route('/mysql/<username>', methods=['POST'])
@ -63,6 +71,22 @@ def create_postgresql_db(auth_user: str, username: str):
return create_db_from_type('postgresql', username)
@bp.route('/mysql/<username>/pwreset', methods=['POST'])
@requires_authentication_no_realm
def reset_mysql_db_passwd(auth_user: str, username: str):
if not (auth_user == username or user_is_in_group(auth_user, 'syscom')):
return {'error': "not authorized to request password reset for others"}, 403
return reset_db_passwd_from_type('mysql', username)
@bp.route('/postgresql/<username>/pwreset', methods=['POST'])
@requires_authentication_no_realm
def reset_postgresql_db_passwd(auth_user: str, username: str):
if not (auth_user == username or user_is_in_group(auth_user, 'syscom')):
return {'error': "not authorized to request password reset for others"}, 403
return reset_db_passwd_from_type('postgresql', username)
@bp.route('/mysql/<username>', methods=['DELETE'])
@authz_restrict_to_syscom
@development_only

@ -1,9 +1,13 @@
from zope.interface import implementer
from zope import component
from contextlib import contextmanager
from ceo_common.interfaces import IDatabaseService, IConfig
from ceo_common.errors import DatabaseConnectionError, DatabasePermissionError
from ceo_common.errors import DatabaseConnectionError, DatabasePermissionError, UserAlreadyExistsError, \
UserNotFoundError
from ceod.utils import gen_password
from ceod.db.utils import response_is_empty
from mysql.connector import connect
from mysql.connector.errors import InterfaceError, ProgrammingError
@ -18,48 +22,58 @@ class MySQLService:
self.auth_username = config.get('mysql_username')
self.auth_password = config.get('mysql_password')
@contextmanager
def mysql_connection(self):
try:
with connect(
host='localhost',
user=self.auth_username,
password=self.auth_password,
) as con:
yield con
except InterfaceError:
raise DatabaseConnectionError()
except ProgrammingError:
raise DatabasePermissionError()
def create_db(self, username: str) -> str:
password = gen_password()
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}'@'localhost' IDENTIFIED BY %(password)s"
create_database = f"""
CREATE DATABASE {username};
GRANT ALL PRIVILEGES ON {username}.* TO '{username}'@'localhost';
"""
try:
with connect(
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})
else:
cursor.execute(reset_password, {'password': password})
with self.mysql_connection() as con:
with con.cursor() as cursor:
if response_is_empty(search_for_user, con):
cursor.execute(create_user, {'password': password})
if response_is_empty(search_for_db, con):
cursor.execute(create_database)
return password
except InterfaceError:
raise DatabaseConnectionError()
except ProgrammingError:
raise DatabasePermissionError()
else:
raise UserAlreadyExistsError()
return password
def reset_db_passwd(self, username: str) -> str:
password = gen_password()
search_for_user = f"SELECT user FROM mysql.user WHERE user='{username}'"
reset_password = f"ALTER USER '{username}'@'localhost' IDENTIFIED BY %(password)s"
with self.mysql_connection() as con:
with con.cursor() as cursor:
if not response_is_empty(search_for_user, con):
cursor.execute(reset_password, {'password': password})
else:
raise UserNotFoundError(username)
return password
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_db)
cursor.execute(drop_user)
except InterfaceError:
raise DatabaseConnectionError()
except ProgrammingError:
raise DatabasePermissionError()
with self.mysql_connection() as con:
with con.cursor() as cursor:
cursor.execute(drop_db)
cursor.execute(drop_user)

@ -1,9 +1,13 @@
from zope.interface import implementer
from zope import component
from contextlib import contextmanager
from ceo_common.interfaces import IDatabaseService, IConfig
from ceo_common.errors import DatabaseConnectionError, DatabasePermissionError
from ceo_common.errors import DatabaseConnectionError, DatabasePermissionError, UserAlreadyExistsError, \
UserNotFoundError
from ceod.utils import gen_password
from ceod.db.utils import response_is_empty
from psycopg2 import connect, OperationalError, ProgrammingError
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
@ -18,16 +22,8 @@ class PostgreSQLService:
self.auth_username = config.get('postgresql_username')
self.auth_password = config.get('postgresql_password')
def create_db(self, username: str) -> str:
password = gen_password()
search_for_user = f"SELECT FROM pg_roles WHERE rolname='{username}'"
search_for_db = f"SELECT FROM pg_database WHERE datname='{username}'"
create_user = f"CREATE USER {username} WITH PASSWORD %(password)s"
reset_password = f"ALTER USER {username} WITH PASSWORD %(password)s"
create_database = f"""
CREATE DATABASE {username} OWNER {username};
REVOKE ALL ON DATABASE {username} FROM PUBLIC;
"""
@contextmanager
def psql_connection(self):
try:
with connect(
host='localhost',
@ -35,35 +31,50 @@ class PostgreSQLService:
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})
else:
cursor.execute(reset_password, {'password': password})
if response_is_empty(search_for_db, con):
cursor.execute(create_database)
return password
yield con
except OperationalError:
raise DatabaseConnectionError()
except ProgrammingError:
raise DatabasePermissionError()
def create_db(self, username: str) -> str:
password = gen_password()
search_for_user = f"SELECT FROM pg_roles WHERE rolname='{username}'"
search_for_db = f"SELECT FROM pg_database WHERE datname='{username}'"
create_user = f"CREATE USER {username} WITH PASSWORD %(password)s"
create_database = f"""
CREATE DATABASE {username} OWNER {username};
REVOKE ALL ON DATABASE {username} FROM PUBLIC;
"""
with self.psql_connection() as con:
with con.cursor() as cursor:
if response_is_empty(search_for_user, con):
cursor.execute(create_user, {'password': password})
if response_is_empty(search_for_db, con):
cursor.execute(create_database)
else:
raise UserAlreadyExistsError()
return password
def reset_db_passwd(self, username: str) -> str:
password = gen_password()
search_for_user = f"SELECT FROM pg_roles WHERE rolname='{username}'"
reset_password = f"ALTER USER {username} WITH PASSWORD %(password)s"
with self.psql_connection() as con:
with con.cursor() as cursor:
if not response_is_empty(search_for_user, con):
cursor.execute(reset_password, {'password': password})
else:
raise UserNotFoundError(username)
return password
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_db)
cursor.execute(drop_user)
except OperationalError:
raise DatabaseConnectionError()
except ProgrammingError:
raise DatabasePermissionError()
with self.psql_connection() as con:
with con.cursor() as cursor:
cursor.execute(drop_db)
cursor.execute(drop_user)

@ -30,6 +30,23 @@ def test_mysql_db_create(cfg):
password=password,
)
def test_mysql_passwd_reset():
pass
# test with curl
# test with invalid perms for curl
# test perms
# test with dup user
# test with invalid perms for db
# test with invalid host for db
# except InterfaceError:
# raise DatabaseConnectionError()
# except ProgrammingError:
@ -41,6 +58,3 @@ def test_mysql_db_create(cfg):
# 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)

Loading…
Cancel
Save