add pwreset endpoint
continuous-integration/drone/pr Build is failing
Details
continuous-integration/drone/pr Build is failing
Details
This commit is contained in:
parent
6421a93459
commit
ef3d130f78
|
@ -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')
|
||||
|
||||
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})
|
||||
if response_is_empty(search_for_db, con):
|
||||
cursor.execute(create_database)
|
||||
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}"
|
||||
@contextmanager
|
||||
def mysql_connection(self):
|
||||
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)
|
||||
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"
|
||||
create_database = f"""
|
||||
CREATE DATABASE {username};
|
||||
GRANT ALL PRIVILEGES ON {username}.* TO '{username}'@'localhost';
|
||||
"""
|
||||
|
||||
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)
|
||||
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}"
|
||||
|
||||
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…
Reference in New Issue