From ba50a39700bea016b95c862b8c3733fe900c4829 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Thu, 2 Sep 2021 00:37:42 -0400 Subject: [PATCH 1/8] database cli --- ceo/cli/database.py | 98 +++++++++++++++++++++++++++++++++++++++++++ ceo/cli/entrypoint.py | 3 ++ 2 files changed, 101 insertions(+) create mode 100644 ceo/cli/database.py diff --git a/ceo/cli/database.py b/ceo/cli/database.py new file mode 100644 index 0000000..3522996 --- /dev/null +++ b/ceo/cli/database.py @@ -0,0 +1,98 @@ +import click +import os + +from zope import component +from ceo_common.interfaces import IConfig + +from ..utils import http_post, http_get +from .utils import handle_sync_response + + +# possible to make default [username] argument the user calling + + +def check_file_path(file): + if os.path.exists(file): + if os.path.isfile(file): + click.echo(f"{file} will be overwritten") + click.confirm('Do you want to continue?', abort=True) + if os.path.isdir(file): + click.echo(f"Error there exists a directory at {file}") + raise click.Abort() + + +def mysql_create_info_file(file, username, password): + cfg_srv = component.getUtility(IConfig) + mysql_host = cfg_srv.get('mysql_host') + info = f"""MySQL Database Information for {username} + + Your new MySQL database was created. To connect, use the following options: + + Database: {username} + Username: {username} + Password: {password} + Host: {mysql_host} + + On {mysql_host} to connect using the MySQL command-line client use + + mysql {username} -u {username} -p + + From other CSC servers you can connect using + + mysql {username} -h {mysql_host} -u {username} -p + """ + with click.open_file(file, "w") as f: + f.write(info) + os.chown(file, username, username) + os.chmod(file, 0o640) + + +def psql_create_info_file(file, username, password): + pass + + +@click.group(short_help='Perform operations on MySQL') +def mysql(): + pass + + +@mysql.command(short_help='Create a MySQL database for the user') +@click.argument('username') +def create(username): + resp = http_get(f'/api/members/{username}') + result = handle_sync_response(resp) + info_file_path = os.path.join(result['home_directory'], "ceo-mysql-info") + + check_file_path(info_file_path) + + resp = http_post(f'/api/db/mysql/{username}') + result = handle_sync_response(resp) + password = result['password'] + + mysql_create_info_file(info_file_path, username, password) + + click.echo(f"""MySQL database {username} with password {password} has been created + The password and more details have been written to {info_file_path}""") + + +@mysql.command(short_help='Reset the password for MySQL user') +@click.argument('username') +def reset(username): + pass + + +@click.group(short_help='Perform operations on PostgreSQL') +def postgresql(): + pass + + +@postgresql.command(short_help='Create a PostgreSQL database for the user') +@click.argument('username') +def create(username): + pass + + +@postgresql.command(short_help='Reset password for PostgreSQL user') +@click.argument('username') +def reset(username): + pass diff --git a/ceo/cli/entrypoint.py b/ceo/cli/entrypoint.py index 04f5306..8993952 100644 --- a/ceo/cli/entrypoint.py +++ b/ceo/cli/entrypoint.py @@ -9,6 +9,7 @@ from ..krb_check import krb_check from .members import members from .groups import groups from .updateprograms import updateprograms +from .database import mysql, postgresql from ceo_common.interfaces import IConfig, IHTTPClient from ceo_common.model import Config, HTTPClient @@ -30,6 +31,8 @@ def cli(ctx): cli.add_command(members) cli.add_command(groups) cli.add_command(updateprograms) +cli.add_command(mysql) +cli.add_command(postgresql) def register_services(): -- 2.39.2 From 14855a55570a4c5ebf8255e4af81e415b263d0e7 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Sat, 4 Sep 2021 00:55:06 -0400 Subject: [PATCH 2/8] add db cli endpoints --- ceo/cli/database.py | 127 +++++++++++++++++++++++++++++++++----------- 1 file changed, 95 insertions(+), 32 deletions(-) diff --git a/ceo/cli/database.py b/ceo/cli/database.py index 3522996..b36e3a6 100644 --- a/ceo/cli/database.py +++ b/ceo/cli/database.py @@ -5,20 +5,21 @@ from zope import component from ceo_common.interfaces import IConfig from ..utils import http_post, http_get -from .utils import handle_sync_response - - -# possible to make default [username] argument the user calling +from .utils import handle_sync_response, Abort def check_file_path(file): - if os.path.exists(file): - if os.path.isfile(file): - click.echo(f"{file} will be overwritten") - click.confirm('Do you want to continue?', abort=True) - if os.path.isdir(file): - click.echo(f"Error there exists a directory at {file}") - raise click.Abort() + if os.path.isfile(file): + click.echo(f"{file} will be overwritten") + click.confirm('Do you want to continue?', abort=True) + elif os.path.isdir(file): + click.echo(f"Error: there exists a directory at {file}") + raise Abort() + + +@click.group(short_help='Perform operations on MySQL') +def mysql(): + pass def mysql_create_info_file(file, username, password): @@ -37,9 +38,10 @@ def mysql_create_info_file(file, username, password): mysql {username} -u {username} -p - From other CSC servers you can connect using + From other CSC machines you can connect using mysql {username} -h {mysql_host} -u {username} -p + """ with click.open_file(file, "w") as f: f.write(info) @@ -47,16 +49,7 @@ def mysql_create_info_file(file, username, password): os.chmod(file, 0o640) -def psql_create_info_file(file, username, password): - pass - - -@click.group(short_help='Perform operations on MySQL') -def mysql(): - pass - - -@mysql.command(short_help='Create a MySQL database for the user') +@mysql.command(short_help='Create a MySQL database for a user') @click.argument('username') def create(username): resp = http_get(f'/api/members/{username}') @@ -71,14 +64,29 @@ def create(username): mysql_create_info_file(info_file_path, username, password) - click.echo(f"""MySQL database {username} with password {password} has been created - The password and more details have been written to {info_file_path}""") + click.echo(f""" + MySQL database {username} with password {password} has been created + This password and more details have been written to {info_file_path}""") -@mysql.command(short_help='Reset the password for MySQL user') +@mysql.command(short_help='Reset the password of a MySQL user') @click.argument('username') -def reset(username): - pass +def pwreset(username): + resp = http_get(f'/api/members/{username}') + result = handle_sync_response(resp) + info_file_path = os.path.join(result['home_directory'], "ceo-mysql-info") + + check_file_path(info_file_path) + + resp = http_post(f'/api/db/mysql/{username}/pwreset') + result = handle_sync_response(resp) + password = result['password'] + + mysql_create_info_file(info_file_path, username, password) + + click.echo(f""" + MySQL database {username} now has the password {password} + This password and more details have been written to {info_file_path}""") @click.group(short_help='Perform operations on PostgreSQL') @@ -86,13 +94,68 @@ def postgresql(): pass -@postgresql.command(short_help='Create a PostgreSQL database for the user') +def psql_create_info_file(file, username, password): + cfg_srv = component.getUtility(IConfig) + psql_host = cfg_srv.get('postgresql_host') + info = f"""PostgreSQL Database Information for {username} + + Your new PostgreSQL database was created. To connect, use the following options: + + Database: {username} + Username: {username} + Password: {password} + Host: {psql_host} + + On {psql_host} to connect using the PostgreSQL command-line client use + + psql -d {username} -U {username} -W + + From other CSC machines you can connect using + + psql -d {username} -h {psql_host} -U {username} -W + + """ + with click.open_file(file, "w") as f: + f.write(info) + os.chown(file, username, username) + os.chmod(file, 0o640) + + +@postgresql.command(short_help='Create a PostgreSQL database for a user') @click.argument('username') def create(username): - pass + resp = http_get(f'/api/members/{username}') + result = handle_sync_response(resp) + info_file_path = os.path.join(result['home_directory'], "ceo-psql-info") + + check_file_path(info_file_path) + + resp = http_post(f'/api/db/postgresql/{username}') + result = handle_sync_response(resp) + password = result['password'] + + psql_create_info_file(info_file_path, username, password) + + click.echo(f""" + PostgreSQL database {username} with password {password} has been created + This password and more details have been written to {info_file_path}""") -@postgresql.command(short_help='Reset password for PostgreSQL user') +@postgresql.command(short_help='Reset the password of a PostgreSQL user') @click.argument('username') -def reset(username): - pass +def pwreset(username): + resp = http_get(f'/api/members/{username}') + result = handle_sync_response(resp) + info_file_path = os.path.join(result['home_directory'], "ceo-psql-info") + + check_file_path(info_file_path) + + resp = http_post(f'/api/db/postgresql/{username}/pwreset') + result = handle_sync_response(resp) + password = result['password'] + + psql_create_info_file(info_file_path, username, password) + + click.echo(f""" + PostgreSQL database {username} now has the password {password} + This password and more details have been written to {info_file_path}""") -- 2.39.2 From 7b49f7016065737ad9ba51af890246b11a183189 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Sat, 4 Sep 2021 01:34:53 -0400 Subject: [PATCH 3/8] fix lint errors --- ceo/cli/database.py | 161 ------------------------------------------ ceo/cli/entrypoint.py | 3 +- ceo/cli/mysql.py | 80 +++++++++++++++++++++ ceo/cli/postgresql.py | 80 +++++++++++++++++++++ ceo/cli/utils.py | 10 +++ 5 files changed, 172 insertions(+), 162 deletions(-) delete mode 100644 ceo/cli/database.py create mode 100644 ceo/cli/mysql.py create mode 100644 ceo/cli/postgresql.py diff --git a/ceo/cli/database.py b/ceo/cli/database.py deleted file mode 100644 index b36e3a6..0000000 --- a/ceo/cli/database.py +++ /dev/null @@ -1,161 +0,0 @@ -import click -import os - -from zope import component -from ceo_common.interfaces import IConfig - -from ..utils import http_post, http_get -from .utils import handle_sync_response, Abort - - -def check_file_path(file): - if os.path.isfile(file): - click.echo(f"{file} will be overwritten") - click.confirm('Do you want to continue?', abort=True) - elif os.path.isdir(file): - click.echo(f"Error: there exists a directory at {file}") - raise Abort() - - -@click.group(short_help='Perform operations on MySQL') -def mysql(): - pass - - -def mysql_create_info_file(file, username, password): - cfg_srv = component.getUtility(IConfig) - mysql_host = cfg_srv.get('mysql_host') - info = f"""MySQL Database Information for {username} - - Your new MySQL database was created. To connect, use the following options: - - Database: {username} - Username: {username} - Password: {password} - Host: {mysql_host} - - On {mysql_host} to connect using the MySQL command-line client use - - mysql {username} -u {username} -p - - From other CSC machines you can connect using - - mysql {username} -h {mysql_host} -u {username} -p - - """ - with click.open_file(file, "w") as f: - f.write(info) - os.chown(file, username, username) - os.chmod(file, 0o640) - - -@mysql.command(short_help='Create a MySQL database for a user') -@click.argument('username') -def create(username): - resp = http_get(f'/api/members/{username}') - result = handle_sync_response(resp) - info_file_path = os.path.join(result['home_directory'], "ceo-mysql-info") - - check_file_path(info_file_path) - - resp = http_post(f'/api/db/mysql/{username}') - result = handle_sync_response(resp) - password = result['password'] - - mysql_create_info_file(info_file_path, username, password) - - click.echo(f""" - MySQL database {username} with password {password} has been created - This password and more details have been written to {info_file_path}""") - - -@mysql.command(short_help='Reset the password of a MySQL user') -@click.argument('username') -def pwreset(username): - resp = http_get(f'/api/members/{username}') - result = handle_sync_response(resp) - info_file_path = os.path.join(result['home_directory'], "ceo-mysql-info") - - check_file_path(info_file_path) - - resp = http_post(f'/api/db/mysql/{username}/pwreset') - result = handle_sync_response(resp) - password = result['password'] - - mysql_create_info_file(info_file_path, username, password) - - click.echo(f""" - MySQL database {username} now has the password {password} - This password and more details have been written to {info_file_path}""") - - -@click.group(short_help='Perform operations on PostgreSQL') -def postgresql(): - pass - - -def psql_create_info_file(file, username, password): - cfg_srv = component.getUtility(IConfig) - psql_host = cfg_srv.get('postgresql_host') - info = f"""PostgreSQL Database Information for {username} - - Your new PostgreSQL database was created. To connect, use the following options: - - Database: {username} - Username: {username} - Password: {password} - Host: {psql_host} - - On {psql_host} to connect using the PostgreSQL command-line client use - - psql -d {username} -U {username} -W - - From other CSC machines you can connect using - - psql -d {username} -h {psql_host} -U {username} -W - - """ - with click.open_file(file, "w") as f: - f.write(info) - os.chown(file, username, username) - os.chmod(file, 0o640) - - -@postgresql.command(short_help='Create a PostgreSQL database for a user') -@click.argument('username') -def create(username): - resp = http_get(f'/api/members/{username}') - result = handle_sync_response(resp) - info_file_path = os.path.join(result['home_directory'], "ceo-psql-info") - - check_file_path(info_file_path) - - resp = http_post(f'/api/db/postgresql/{username}') - result = handle_sync_response(resp) - password = result['password'] - - psql_create_info_file(info_file_path, username, password) - - click.echo(f""" - PostgreSQL database {username} with password {password} has been created - This password and more details have been written to {info_file_path}""") - - -@postgresql.command(short_help='Reset the password of a PostgreSQL user') -@click.argument('username') -def pwreset(username): - resp = http_get(f'/api/members/{username}') - result = handle_sync_response(resp) - info_file_path = os.path.join(result['home_directory'], "ceo-psql-info") - - check_file_path(info_file_path) - - resp = http_post(f'/api/db/postgresql/{username}/pwreset') - result = handle_sync_response(resp) - password = result['password'] - - psql_create_info_file(info_file_path, username, password) - - click.echo(f""" - PostgreSQL database {username} now has the password {password} - This password and more details have been written to {info_file_path}""") diff --git a/ceo/cli/entrypoint.py b/ceo/cli/entrypoint.py index 8993952..bf11edd 100644 --- a/ceo/cli/entrypoint.py +++ b/ceo/cli/entrypoint.py @@ -9,7 +9,8 @@ from ..krb_check import krb_check from .members import members from .groups import groups from .updateprograms import updateprograms -from .database import mysql, postgresql +from .mysql import mysql +from .postgresql import postgresql from ceo_common.interfaces import IConfig, IHTTPClient from ceo_common.model import Config, HTTPClient diff --git a/ceo/cli/mysql.py b/ceo/cli/mysql.py new file mode 100644 index 0000000..30786c3 --- /dev/null +++ b/ceo/cli/mysql.py @@ -0,0 +1,80 @@ +import click +import os + +from zope import component +from ceo_common.interfaces import IConfig + +from ..utils import http_post, http_get +from .utils import handle_sync_response, check_file_path + + +def mysql_create_info_file(file, username, password): + cfg_srv = component.getUtility(IConfig) + mysql_host = cfg_srv.get('mysql_host') + info = f"""MySQL Database Information for {username} + + Your new MySQL database was created. To connect, use the following options: + + Database: {username} + Username: {username} + Password: {password} + Host: {mysql_host} + + On {mysql_host} to connect using the MySQL command-line client use + + mysql {username} -u {username} -p + + From other CSC machines you can connect using + + mysql {username} -h {mysql_host} -u {username} -p + + """ + with click.open_file(file, "w") as f: + f.write(info) + os.chown(file, username, username) + os.chmod(file, 0o640) + + +@click.group(short_help='Perform operations on MySQL') +def mysql(): + pass + + +@mysql.command(short_help='Create a MySQL database for a user') +@click.argument('username') +def create(username): + resp = http_get(f'/api/members/{username}') + result = handle_sync_response(resp) + info_file_path = os.path.join(result['home_directory'], "ceo-mysql-info") + + check_file_path(info_file_path) + + resp = http_post(f'/api/db/mysql/{username}') + result = handle_sync_response(resp) + password = result['password'] + + mysql_create_info_file(info_file_path, username, password) + + click.echo(f""" + MySQL database {username} with password {password} has been created + This password and more details have been written to {info_file_path}""") + + +@mysql.command(short_help='Reset the password of a MySQL user') +@click.argument('username') +def pwreset(username): + resp = http_get(f'/api/members/{username}') + result = handle_sync_response(resp) + info_file_path = os.path.join(result['home_directory'], "ceo-mysql-info") + + check_file_path(info_file_path) + + resp = http_post(f'/api/db/mysql/{username}/pwreset') + result = handle_sync_response(resp) + password = result['password'] + + mysql_create_info_file(info_file_path, username, password) + + click.echo(f""" + MySQL database {username} now has the password {password} + This password and more details have been written to {info_file_path}""") diff --git a/ceo/cli/postgresql.py b/ceo/cli/postgresql.py new file mode 100644 index 0000000..7127ac8 --- /dev/null +++ b/ceo/cli/postgresql.py @@ -0,0 +1,80 @@ +import click +import os + +from zope import component +from ceo_common.interfaces import IConfig + +from ..utils import http_post, http_get +from .utils import handle_sync_response, check_file_path + + +def psql_create_info_file(file, username, password): + cfg_srv = component.getUtility(IConfig) + psql_host = cfg_srv.get('postgresql_host') + info = f"""PostgreSQL Database Information for {username} + + Your new PostgreSQL database was created. To connect, use the following options: + + Database: {username} + Username: {username} + Password: {password} + Host: {psql_host} + + On {psql_host} to connect using the PostgreSQL command-line client use + + psql -d {username} -U {username} -W + + From other CSC machines you can connect using + + psql -d {username} -h {psql_host} -U {username} -W + + """ + with click.open_file(file, "w") as f: + f.write(info) + os.chown(file, username, username) + os.chmod(file, 0o640) + + +@click.group(short_help='Perform operations on PostgreSQL') +def postgresql(): + pass + + +@postgresql.command(short_help='Create a PostgreSQL database for a user') +@click.argument('username') +def create(username): + resp = http_get(f'/api/members/{username}') + result = handle_sync_response(resp) + info_file_path = os.path.join(result['home_directory'], "ceo-psql-info") + + check_file_path(info_file_path) + + resp = http_post(f'/api/db/postgresql/{username}') + result = handle_sync_response(resp) + password = result['password'] + + psql_create_info_file(info_file_path, username, password) + + click.echo(f""" + PostgreSQL database {username} with password {password} has been created + This password and more details have been written to {info_file_path}""") + + +@postgresql.command(short_help='Reset the password of a PostgreSQL user') +@click.argument('username') +def pwreset(username): + resp = http_get(f'/api/members/{username}') + result = handle_sync_response(resp) + info_file_path = os.path.join(result['home_directory'], "ceo-psql-info") + + check_file_path(info_file_path) + + resp = http_post(f'/api/db/postgresql/{username}/pwreset') + result = handle_sync_response(resp) + password = result['password'] + + psql_create_info_file(info_file_path, username, password) + + click.echo(f""" + PostgreSQL database {username} now has the password {password} + This password and more details have been written to {info_file_path}""") diff --git a/ceo/cli/utils.py b/ceo/cli/utils.py index 98aa23f..5443c35 100644 --- a/ceo/cli/utils.py +++ b/ceo/cli/utils.py @@ -1,6 +1,7 @@ import json import socket import sys +import os from typing import List, Tuple, Dict import click @@ -114,6 +115,15 @@ def handle_sync_response(resp: requests.Response): return resp.json() +def check_file_path(file): + if os.path.isfile(file): + click.echo(f"{file} will be overwritten") + click.confirm('Do you want to continue?', abort=True) + elif os.path.isdir(file): + click.echo(f"Error: there exists a directory at {file}") + raise Abort() + + def check_if_in_development() -> bool: """Aborts if we are not currently in the dev environment.""" if not socket.getfqdn().endswith('.csclub.internal'): -- 2.39.2 From 9917635dc8f391d0e0cbc8f632ce01e2aca5ca00 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Sat, 4 Sep 2021 23:26:15 -0400 Subject: [PATCH 4/8] catch unable to connect to mysql db --- ceod/db/MySQLService.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ceod/db/MySQLService.py b/ceod/db/MySQLService.py index e6a194d..629af96 100644 --- a/ceod/db/MySQLService.py +++ b/ceod/db/MySQLService.py @@ -10,7 +10,7 @@ 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 +from mysql.connector.errors import OperationalError, ProgrammingError logger = logger_factory(__name__) @@ -35,7 +35,7 @@ class MySQLService: password=self.auth_password, ) as con: yield con - except InterfaceError as e: + except OperationalError as e: logger.error(e) raise DatabaseConnectionError() except ProgrammingError as e: -- 2.39.2 From df412191e3ed27e4dbff9e58bde9c82a466949f1 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Fri, 10 Sep 2021 01:39:46 -0400 Subject: [PATCH 5/8] add tests --- ceo/cli/mysql.py | 36 ++++++++----- ceo/cli/postgresql.py | 36 ++++++++----- ceod/api/database.py | 4 +- ceod/db/MySQLService.py | 2 + ceod/db/PostgreSQLService.py | 2 + tests/ceo/cli/test_db_mysql.py | 77 ++++++++++++++++++++++++++++ tests/ceo/cli/test_db_postgresql.py | 79 +++++++++++++++++++++++++++++ 7 files changed, 210 insertions(+), 26 deletions(-) create mode 100644 tests/ceo/cli/test_db_mysql.py create mode 100644 tests/ceo/cli/test_db_postgresql.py diff --git a/ceo/cli/mysql.py b/ceo/cli/mysql.py index 30786c3..4098ea1 100644 --- a/ceo/cli/mysql.py +++ b/ceo/cli/mysql.py @@ -4,11 +4,11 @@ import os from zope import component from ceo_common.interfaces import IConfig -from ..utils import http_post, http_get -from .utils import handle_sync_response, check_file_path +from ..utils import http_post, http_get, http_delete +from .utils import handle_sync_response, check_file_path, check_if_in_development -def mysql_create_info_file(file, username, password): +def mysql_cli_response(file, username, password): cfg_srv = component.getUtility(IConfig) mysql_host = cfg_srv.get('mysql_host') info = f"""MySQL Database Information for {username} @@ -34,6 +34,17 @@ def mysql_create_info_file(file, username, password): os.chown(file, username, username) os.chmod(file, 0o640) + click.echo(f"""MySQL database created + + Connection Information: + + Database: {username} + Username: {username} + Password: {password} + Host: {mysql_host} + + Settings and more info has been written to {file}""") + @click.group(short_help='Perform operations on MySQL') def mysql(): @@ -53,11 +64,7 @@ def create(username): result = handle_sync_response(resp) password = result['password'] - mysql_create_info_file(info_file_path, username, password) - - click.echo(f""" - MySQL database {username} with password {password} has been created - This password and more details have been written to {info_file_path}""") + mysql_cli_response(info_file_path, username, password) @mysql.command(short_help='Reset the password of a MySQL user') @@ -73,8 +80,13 @@ def pwreset(username): result = handle_sync_response(resp) password = result['password'] - mysql_create_info_file(info_file_path, username, password) + mysql_cli_response(info_file_path, username, password) - click.echo(f""" - MySQL database {username} now has the password {password} - This password and more details have been written to {info_file_path}""") + +@mysql.command(short_help="Delete the database of a MySQL user") +@click.argument('username') +def delete(username): + check_if_in_development() + click.confirm(f"Are you sure?", abort=True) + resp = http_delete(f'/api/db/mysql/{username}') + handle_sync_response(resp) diff --git a/ceo/cli/postgresql.py b/ceo/cli/postgresql.py index 7127ac8..4807d7e 100644 --- a/ceo/cli/postgresql.py +++ b/ceo/cli/postgresql.py @@ -4,11 +4,11 @@ import os from zope import component from ceo_common.interfaces import IConfig -from ..utils import http_post, http_get -from .utils import handle_sync_response, check_file_path +from ..utils import http_post, http_get, http_delete +from .utils import handle_sync_response, check_file_path, check_if_in_development -def psql_create_info_file(file, username, password): +def psql_cli_response(file, username, password): cfg_srv = component.getUtility(IConfig) psql_host = cfg_srv.get('postgresql_host') info = f"""PostgreSQL Database Information for {username} @@ -34,6 +34,17 @@ def psql_create_info_file(file, username, password): os.chown(file, username, username) os.chmod(file, 0o640) + click.echo(f"""PostgreSQL database created + + Connection Information: + + Database: {username} + Username: {username} + Password: {password} + Host: {psql_host} + + Settings and more info has been written to {file}""") + @click.group(short_help='Perform operations on PostgreSQL') def postgresql(): @@ -53,11 +64,7 @@ def create(username): result = handle_sync_response(resp) password = result['password'] - psql_create_info_file(info_file_path, username, password) - - click.echo(f""" - PostgreSQL database {username} with password {password} has been created - This password and more details have been written to {info_file_path}""") + psql_cli_response(info_file_path, username, password) @postgresql.command(short_help='Reset the password of a PostgreSQL user') @@ -73,8 +80,13 @@ def pwreset(username): result = handle_sync_response(resp) password = result['password'] - psql_create_info_file(info_file_path, username, password) + psql_cli_response(info_file_path, username, password) - click.echo(f""" - PostgreSQL database {username} now has the password {password} - This password and more details have been written to {info_file_path}""") + +@postgresql.command(short_help="Delete the database of a PostgreSQL user") +@click.argument('username') +def delete(username): + check_if_in_development() + click.confirm(f"Are you sure?", abort=True) + resp = http_delete(f'/api/db/postgresql/{username}') + handle_sync_response(resp) diff --git a/ceod/api/database.py b/ceod/api/database.py index ca81367..94e9dc0 100644 --- a/ceod/api/database.py +++ b/ceod/api/database.py @@ -31,9 +31,9 @@ def db_exception_handler(func): except InvalidUsernameError: return {'error': 'username contains invalid characters'}, 400 except DatabaseConnectionError: - return {'error': 'unable to connect or authenticate to sql server'}, 500 + return {'error': 'unable to connect to sql server'}, 500 except DatabasePermissionError: - return {'error': 'unable to perform action due to permissions'}, 500 + return {'error': 'unable to connect or action failed due to permissions'}, 500 return function diff --git a/ceod/db/MySQLService.py b/ceod/db/MySQLService.py index 629af96..10889c9 100644 --- a/ceod/db/MySQLService.py +++ b/ceod/db/MySQLService.py @@ -35,9 +35,11 @@ class MySQLService: password=self.auth_password, ) as con: yield con + # unable to connect except OperationalError as e: logger.error(e) raise DatabaseConnectionError() + # invalid credentials / user does not exist / invalid permissions for action except ProgrammingError as e: logger.error(e) raise DatabasePermissionError() diff --git a/ceod/db/PostgreSQLService.py b/ceod/db/PostgreSQLService.py index 3f3cbb8..e01e3e1 100644 --- a/ceod/db/PostgreSQLService.py +++ b/ceod/db/PostgreSQLService.py @@ -39,9 +39,11 @@ class PostgreSQLService: ) con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) yield con + # unable to connect / invalid credentials / user does not exist except OperationalError as e: logger.error(e) raise DatabaseConnectionError() + # invalid permissions for action except ProgrammingError as e: logger.error(e) raise DatabasePermissionError() diff --git a/tests/ceo/cli/test_db_mysql.py b/tests/ceo/cli/test_db_mysql.py new file mode 100644 index 0000000..758e9c9 --- /dev/null +++ b/tests/ceo/cli/test_db_mysql.py @@ -0,0 +1,77 @@ +import pytest, os + +from click.testing import CliRunner +from ceo.cli import cli + +from mysql.connector import connect +from mysql.connector.errors import ProgrammingError + + +def mysql_attempt_connection(host, username, password): + with connect( + host=host, + user=username, + password=password, + ) as con, con.cursor() as cur: + cur.execute("SHOW DATABASES") + response = cur.fetchall() + assert len(response) == 2 + + with pytest.raises(ProgrammingError): + cur.execute("CREATE DATABASE new_db") + + +def test_mysql(cli_setup, cfg, ldap_user): + runner = CliRunner() + + username = ldap_user.uid + host = cfg.get("mysql_host") + info_file_path = os.path.join(ldap_user.home_directory, "ceo-mysql-info") + assert not os.path.isfile(info_file_path) + + # create database for user + result = runner.invoke(cli, ['mysql', 'create', username]) + assert result.exit_code == 0 + assert os.path.isfile(info_file_path) + + response_arr = result.output.split() + passwd = response_arr[response_arr.index("Password:") + 1] + with open(info_file_path, 'r') as file: + old_info = file.read() + + expected = f"""MySQL database created + + Connection Information: + + Database: {username} + Username: {username} + Password: {passwd} + Host: {host} + + Settings and more info has been written to {info_file_path}""" + + assert result.output == expected + mysql_attempt_connection(host, username, passwd) + + # perform password reset for user + result = runner.invoke(cli, ['mysql', 'pwreset', username], input="y\n") + assert result.exit_code == 0 + + response_arr = result.output.split() + new_passwd = response_arr[response_arr.index("Password:") + 1] + with open(info_file_path, 'r') as file: + new_info = file.read() + + assert new_passwd != passwd + assert old_info != new_info + mysql_attempt_connection(host, username, new_passwd) + + # delete database and file + result = runner.invoke(cli, ['mysql', 'delete', username], input="y\n") + assert result.exit_code == 0 + + # user should be deleted + with pytest.raises(ProgrammingError): + mysql_attempt_connection(host, username, passwd) + + os.remove(info_file_path) diff --git a/tests/ceo/cli/test_db_postgresql.py b/tests/ceo/cli/test_db_postgresql.py new file mode 100644 index 0000000..06f47c2 --- /dev/null +++ b/tests/ceo/cli/test_db_postgresql.py @@ -0,0 +1,79 @@ +import pytest, os + +from click.testing import CliRunner +from ceo.cli import cli + +from psycopg2 import connect, OperationalError, ProgrammingError + + +def psql_attempt_connection(host, username, password): + con = connect( + host=host, + user=username, + password=password, + ) + con.autocommit = True + with con.cursor() as cur: + cur.execute("SELECT datname FROM pg_database") + response = cur.fetchall() + # 3 of the 4 are postgres, template0, template1 + assert len(response) == 4 + with pytest.raises(ProgrammingError): + cur.execute("CREATE DATABASE new_db") + con.close() + + +def test_postgresql(cli_setup, cfg, ldap_user): + runner = CliRunner() + + username = ldap_user.uid + host = cfg.get("postgresql_host") + info_file_path = os.path.join(ldap_user.home_directory, "ceo-psql-info") + assert not os.path.isfile(info_file_path) + + # create database for user + result = runner.invoke(cli, ['postgresql', 'create', username]) + assert result.exit_code == 0 + assert os.path.isfile(info_file_path) + + response_arr = result.output.split() + passwd = response_arr[response_arr.index("Password:") + 1] + with open(info_file_path, 'r') as file: + old_info = file.read() + + expected = f"""PostgreSQL database created + + Connection Information: + + Database: {username} + Username: {username} + Password: {passwd} + Host: {host} + + Settings and more info has been written to {info_file_path}""" + + assert result.output == expected + psql_attempt_connection(host, username, passwd) + + # perform password reset for user + result = runner.invoke(cli, ['postgresql', 'pwreset', username], input="y\n") + assert result.exit_code == 0 + + response_arr = result.output.split() + new_passwd = response_arr[response_arr.index("Password:") + 1] + with open(info_file_path, 'r') as file: + new_info = file.read() + + assert new_passwd != passwd + assert old_info != new_info + psql_attempt_connection(host, username, new_passwd) + + # delete database and file + result = runner.invoke(cli, ['postgresql', 'delete', username], input="y\n") + assert result.exit_code == 0 + + # user should be deleted + with pytest.raises(OperationalError): + psql_attempt_connection(host, username, passwd) + + os.remove(info_file_path) -- 2.39.2 From 0d94b1fafee7944797d7c631bce1fd7a7397b9d6 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Fri, 10 Sep 2021 01:44:31 -0400 Subject: [PATCH 6/8] fix for flake8 --- ceo/cli/mysql.py | 2 +- ceo/cli/postgresql.py | 2 +- tests/ceo/cli/test_db_mysql.py | 3 ++- tests/ceo/cli/test_db_postgresql.py | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ceo/cli/mysql.py b/ceo/cli/mysql.py index 4098ea1..269dbdd 100644 --- a/ceo/cli/mysql.py +++ b/ceo/cli/mysql.py @@ -87,6 +87,6 @@ def pwreset(username): @click.argument('username') def delete(username): check_if_in_development() - click.confirm(f"Are you sure?", abort=True) + click.confirm("Are you sure?", abort=True) resp = http_delete(f'/api/db/mysql/{username}') handle_sync_response(resp) diff --git a/ceo/cli/postgresql.py b/ceo/cli/postgresql.py index 4807d7e..468d101 100644 --- a/ceo/cli/postgresql.py +++ b/ceo/cli/postgresql.py @@ -87,6 +87,6 @@ def pwreset(username): @click.argument('username') def delete(username): check_if_in_development() - click.confirm(f"Are you sure?", abort=True) + click.confirm("Are you sure?", abort=True) resp = http_delete(f'/api/db/postgresql/{username}') handle_sync_response(resp) diff --git a/tests/ceo/cli/test_db_mysql.py b/tests/ceo/cli/test_db_mysql.py index 758e9c9..699c6a7 100644 --- a/tests/ceo/cli/test_db_mysql.py +++ b/tests/ceo/cli/test_db_mysql.py @@ -1,4 +1,5 @@ -import pytest, os +import pytest +import os from click.testing import CliRunner from ceo.cli import cli diff --git a/tests/ceo/cli/test_db_postgresql.py b/tests/ceo/cli/test_db_postgresql.py index 06f47c2..f79d88d 100644 --- a/tests/ceo/cli/test_db_postgresql.py +++ b/tests/ceo/cli/test_db_postgresql.py @@ -1,4 +1,5 @@ -import pytest, os +import pytest +import os from click.testing import CliRunner from ceo.cli import cli -- 2.39.2 From 1cd07c228ce976e9f45e358981cee05bcaa4339a Mon Sep 17 00:00:00 2001 From: Max Erenberg <> Date: Sat, 11 Sep 2021 12:52:33 -0400 Subject: [PATCH 7/8] use unix_socket auth for MySQL --- .drone/coffee-setup.sh | 2 +- README.md | 2 +- ceo/cli/database.py | 104 ++++++++++++++++++++++++++++++++++++++++ ceo/cli/mysql.py | 74 ++-------------------------- ceo/cli/postgresql.py | 74 ++-------------------------- ceod/db/MySQLService.py | 24 ++++------ ceod/model/User.py | 13 +++-- tests/ceo_dev.ini | 7 +++ tests/ceod_dev.ini | 2 +- 9 files changed, 140 insertions(+), 162 deletions(-) create mode 100644 ceo/cli/database.py diff --git a/.drone/coffee-setup.sh b/.drone/coffee-setup.sh index e84d137..cc991c4 100755 --- a/.drone/coffee-setup.sh +++ b/.drone/coffee-setup.sh @@ -30,7 +30,7 @@ host all postgres 0.0.0.0/0 md5 local all all peer host all all localhost md5 -local sameuser all md5 +local sameuser all peer host sameuser all 0.0.0.0/0 md5 EOF grep -Eq "^listen_addresses = '*'$" $POSTGRES_DIR/postgresql.conf || \ diff --git a/README.md b/README.md index 2a8e1b9..0ad696c 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ host all postgres 0.0.0.0/0 md5 local all all peer host all all localhost md5 -local sameuser all md5 +local sameuser all peer host sameuser all 0.0.0.0/0 md5 ``` **Warning**: in prod, the postgres user should only be allowed to connect locally, diff --git a/ceo/cli/database.py b/ceo/cli/database.py new file mode 100644 index 0000000..6a7571b --- /dev/null +++ b/ceo/cli/database.py @@ -0,0 +1,104 @@ +import os +from typing import Dict + +import click +from zope import component + +from ..utils import http_post, http_get, http_delete +from .utils import handle_sync_response, check_file_path, check_if_in_development +from ceo_common.interfaces import IConfig + + +def db_cli_response(filename: str, user_dict: Dict, password: str, db_type: str, op: str): + cfg_srv = component.getUtility(IConfig) + db_host = cfg_srv.get(f'{db_type}_host') + username = user_dict['uid'] + if db_type == 'mysql': + db_type_name = 'MySQL' + db_cli_local_cmd = f'mysql {username}' + db_cli_cmd = f'mysql {username} -h {db_host} -u {username} -p' + else: + db_type_name = 'PostgreSQL' + db_cli_local_cmd = f'psql {username}' + db_cli_cmd = f'psql -d {username} -h {db_host} -U {username} -W' + username = user_dict['uid'] + info = f"""{db_type_name} Database Information for {username} + +Your new {db_type_name} database was created. To connect, use the following options: + +Database: {username} +Username: {username} +Password: {password} +Host: {db_host} + +On {db_host} to connect using the {db_type_name} command-line client use + + {db_cli_local_cmd} + +From other CSC machines you can connect using + + {db_cli_cmd} +""" + wrote_to_file = False + try: + # TODO: use phosphoric-acid to write to file (phosphoric-acid makes + # internal API call to caffeine) + with click.open_file(filename, "w") as f: + f.write(info) + os.chown(filename, user_dict['uid_number'], user_dict['gid_number']) + os.chmod(filename, 0o640) + wrote_to_file = True + except PermissionError: + pass + if op == 'create': + click.echo(f'{db_type_name} database created.') + click.echo(f'''Connection Information: + +Database: {username} +Username: {username} +Password: {password} +Host: {db_host}''') + if wrote_to_file: + click.echo(f"\nThese settings have been written to {filename}.") + else: + click.echo(f"\nWe were unable to write these settings to {filename}.") + + +def create(username: str, db_type: str): + db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL' + resp = http_get(f'/api/members/{username}') + user_dict = handle_sync_response(resp) + click.confirm(f'Are you sure you want to create a {db_type_name} database for {username}?', abort=True) + + info_file_path = os.path.join(user_dict['home_directory'], f"ceo-{db_type}-info") + check_file_path(info_file_path) + + resp = http_post(f'/api/db/{db_type}/{username}') + result = handle_sync_response(resp) + password = result['password'] + + db_cli_response(info_file_path, user_dict, password, db_type, 'create') + + +def pwreset(username: str, db_type: str): + db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL' + resp = http_get(f'/api/members/{username}') + user_dict = handle_sync_response(resp) + click.confirm(f'Are you sure you want reset the {db_type_name} password for {username}?', abort=True) + + info_file_path = os.path.join(user_dict['home_directory'], f"ceo-{db_type}-info") + check_file_path(info_file_path) + + resp = http_post(f'/api/db/{db_type}/{username}/pwreset') + result = handle_sync_response(resp) + password = result['password'] + + db_cli_response(info_file_path, user_dict, password, db_type, 'pwreset') + + +def delete(username: str, db_type: str): + check_if_in_development() + db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL' + click.confirm(f"Are you sure you want to delete the {db_type_name} database for {username}?", abort=True) + resp = http_delete(f'/api/db/{db_type}/{username}') + handle_sync_response(resp) diff --git a/ceo/cli/mysql.py b/ceo/cli/mysql.py index 269dbdd..c7e099f 100644 --- a/ceo/cli/mysql.py +++ b/ceo/cli/mysql.py @@ -1,49 +1,6 @@ import click -import os -from zope import component -from ceo_common.interfaces import IConfig - -from ..utils import http_post, http_get, http_delete -from .utils import handle_sync_response, check_file_path, check_if_in_development - - -def mysql_cli_response(file, username, password): - cfg_srv = component.getUtility(IConfig) - mysql_host = cfg_srv.get('mysql_host') - info = f"""MySQL Database Information for {username} - - Your new MySQL database was created. To connect, use the following options: - - Database: {username} - Username: {username} - Password: {password} - Host: {mysql_host} - - On {mysql_host} to connect using the MySQL command-line client use - - mysql {username} -u {username} -p - - From other CSC machines you can connect using - - mysql {username} -h {mysql_host} -u {username} -p - - """ - with click.open_file(file, "w") as f: - f.write(info) - os.chown(file, username, username) - os.chmod(file, 0o640) - - click.echo(f"""MySQL database created - - Connection Information: - - Database: {username} - Username: {username} - Password: {password} - Host: {mysql_host} - - Settings and more info has been written to {file}""") +from .database import create as db_create, pwreset as db_pwreset, delete as db_delete @click.group(short_help='Perform operations on MySQL') @@ -54,39 +11,16 @@ def mysql(): @mysql.command(short_help='Create a MySQL database for a user') @click.argument('username') def create(username): - resp = http_get(f'/api/members/{username}') - result = handle_sync_response(resp) - info_file_path = os.path.join(result['home_directory'], "ceo-mysql-info") - - check_file_path(info_file_path) - - resp = http_post(f'/api/db/mysql/{username}') - result = handle_sync_response(resp) - password = result['password'] - - mysql_cli_response(info_file_path, username, password) + db_create(username, 'mysql') @mysql.command(short_help='Reset the password of a MySQL user') @click.argument('username') def pwreset(username): - resp = http_get(f'/api/members/{username}') - result = handle_sync_response(resp) - info_file_path = os.path.join(result['home_directory'], "ceo-mysql-info") - - check_file_path(info_file_path) - - resp = http_post(f'/api/db/mysql/{username}/pwreset') - result = handle_sync_response(resp) - password = result['password'] - - mysql_cli_response(info_file_path, username, password) + db_pwreset(username, 'mysql') @mysql.command(short_help="Delete the database of a MySQL user") @click.argument('username') def delete(username): - check_if_in_development() - click.confirm("Are you sure?", abort=True) - resp = http_delete(f'/api/db/mysql/{username}') - handle_sync_response(resp) + db_delete(username, 'mysql') diff --git a/ceo/cli/postgresql.py b/ceo/cli/postgresql.py index 468d101..bad5723 100644 --- a/ceo/cli/postgresql.py +++ b/ceo/cli/postgresql.py @@ -1,49 +1,6 @@ import click -import os -from zope import component -from ceo_common.interfaces import IConfig - -from ..utils import http_post, http_get, http_delete -from .utils import handle_sync_response, check_file_path, check_if_in_development - - -def psql_cli_response(file, username, password): - cfg_srv = component.getUtility(IConfig) - psql_host = cfg_srv.get('postgresql_host') - info = f"""PostgreSQL Database Information for {username} - - Your new PostgreSQL database was created. To connect, use the following options: - - Database: {username} - Username: {username} - Password: {password} - Host: {psql_host} - - On {psql_host} to connect using the PostgreSQL command-line client use - - psql -d {username} -U {username} -W - - From other CSC machines you can connect using - - psql -d {username} -h {psql_host} -U {username} -W - - """ - with click.open_file(file, "w") as f: - f.write(info) - os.chown(file, username, username) - os.chmod(file, 0o640) - - click.echo(f"""PostgreSQL database created - - Connection Information: - - Database: {username} - Username: {username} - Password: {password} - Host: {psql_host} - - Settings and more info has been written to {file}""") +from .database import create as db_create, pwreset as db_pwreset, delete as db_delete @click.group(short_help='Perform operations on PostgreSQL') @@ -54,39 +11,16 @@ def postgresql(): @postgresql.command(short_help='Create a PostgreSQL database for a user') @click.argument('username') def create(username): - resp = http_get(f'/api/members/{username}') - result = handle_sync_response(resp) - info_file_path = os.path.join(result['home_directory'], "ceo-psql-info") - - check_file_path(info_file_path) - - resp = http_post(f'/api/db/postgresql/{username}') - result = handle_sync_response(resp) - password = result['password'] - - psql_cli_response(info_file_path, username, password) + db_create(username, 'postgresql') @postgresql.command(short_help='Reset the password of a PostgreSQL user') @click.argument('username') def pwreset(username): - resp = http_get(f'/api/members/{username}') - result = handle_sync_response(resp) - info_file_path = os.path.join(result['home_directory'], "ceo-psql-info") - - check_file_path(info_file_path) - - resp = http_post(f'/api/db/postgresql/{username}/pwreset') - result = handle_sync_response(resp) - password = result['password'] - - psql_cli_response(info_file_path, username, password) + db_pwreset(username, 'postgresql') @postgresql.command(short_help="Delete the database of a PostgreSQL user") @click.argument('username') def delete(username): - check_if_in_development() - click.confirm("Are you sure?", abort=True) - resp = http_delete(f'/api/db/postgresql/{username}') - handle_sync_response(resp) + db_delete(username, 'postgresql') diff --git a/ceod/db/MySQLService.py b/ceod/db/MySQLService.py index 10889c9..34bd0bf 100644 --- a/ceod/db/MySQLService.py +++ b/ceod/db/MySQLService.py @@ -49,24 +49,21 @@ class MySQLService: search_for_user = f"SELECT user FROM mysql.user WHERE user='{username}'" search_for_db = f"SHOW DATABASES LIKE '{username}'" # CREATE USER can't be used in a query with multiple statements - create_user_commands = [ - f"CREATE USER '{username}'@'localhost' IDENTIFIED BY %(password)s", - f"CREATE USER '{username}'@'%' IDENTIFIED BY %(password)s", - ] + create_local_user_cmd = f"CREATE USER '{username}'@'localhost' IDENTIFIED VIA unix_socket" + create_user_cmd = f"CREATE USER '{username}'@'%' IDENTIFIED BY %(password)s" create_database = f""" CREATE DATABASE {username}; - GRANT ALL PRIVILEGES ON {username}.* TO '{username}'@'localhost'; + GRANT ALL PRIVILEGES ON {username}.* TO '{username}'@'localhost' IDENTIFIED VIA unix_socket; GRANT ALL PRIVILEGES ON {username}.* TO '{username}'@'%'; """ with self.mysql_connection() as con, con.cursor() as cursor: - if response_is_empty(search_for_user, con): - for cmd in create_user_commands: - cursor.execute(cmd, {'password': password}) - if response_is_empty(search_for_db, con): - cursor.execute(create_database) - else: + if not response_is_empty(search_for_user, con): raise UserAlreadyExistsError() + cursor.execute(create_local_user_cmd) + cursor.execute(create_user_cmd, {'password': password}) + if response_is_empty(search_for_db, con): + cursor.execute(create_database) return password def reset_db_passwd(self, username: str) -> str: @@ -78,10 +75,9 @@ class MySQLService: """ with self.mysql_connection() as con, con.cursor() as cursor: - if not response_is_empty(search_for_user, con): - cursor.execute(reset_password, {'password': password}) - else: + if response_is_empty(search_for_user, con): raise UserNotFoundError(username) + cursor.execute(reset_password, {'password': password}) return password def delete_db(self, username: str): diff --git a/ceod/model/User.py b/ceod/model/User.py index 5bad209..7b57eeb 100644 --- a/ceod/model/User.py +++ b/ceod/model/User.py @@ -56,7 +56,6 @@ class User: self.ldap_srv = component.getUtility(ILDAPService) self.krb_srv = component.getUtility(IKerberosService) - self.file_srv = component.getUtility(IFileService) def to_dict(self, get_forwarding_addresses: bool = False) -> Dict: data = { @@ -103,10 +102,12 @@ class User: self.krb_srv.change_password(self.uid, password) def create_home_dir(self): - self.file_srv.create_home_dir(self) + file_srv = component.getUtility(IFileService) + file_srv.create_home_dir(self) def delete_home_dir(self): - self.file_srv.delete_home_dir(self) + file_srv = component.getUtility(IFileService) + file_srv.delete_home_dir(self) def subscribe_to_mailing_list(self, mailing_list: str): component.getUtility(IMailmanService).subscribe(self.uid, mailing_list) @@ -163,7 +164,9 @@ class User: self.positions = positions def get_forwarding_addresses(self) -> List[str]: - return self.file_srv.get_forwarding_addresses(self) + file_srv = component.getUtility(IFileService) + return file_srv.get_forwarding_addresses(self) def set_forwarding_addresses(self, addresses: List[str]): - self.file_srv.set_forwarding_addresses(self, addresses) + file_srv = component.getUtility(IFileService) + file_srv.set_forwarding_addresses(self, addresses) diff --git a/tests/ceo_dev.ini b/tests/ceo_dev.ini index 978f6f5..08652dc 100644 --- a/tests/ceo_dev.ini +++ b/tests/ceo_dev.ini @@ -5,6 +5,7 @@ uw_domain = uwaterloo.internal [ceod] # this is the host with the ceod/admin Kerberos key admin_host = phosphoric-acid +database_host = coffee use_https = false port = 9987 @@ -12,3 +13,9 @@ port = 9987 required = president,vice-president,sysadmin available = president,vice-president,treasurer,secretary, sysadmin,cro,librarian,imapd,webmaster,offsck + +[mysql] +host = coffee + +[postgresql] +host = coffee diff --git a/tests/ceod_dev.ini b/tests/ceod_dev.ini index f577120..fea17d5 100644 --- a/tests/ceod_dev.ini +++ b/tests/ceod_dev.ini @@ -13,7 +13,7 @@ port = 9987 [ldap] admin_principal = ceod/admin -server_url = ldap://ldap-master.csclub.internal +server_url = ldap://auth1.csclub.internal sasl_realm = CSCLUB.INTERNAL users_base = ou=People,dc=csclub,dc=internal groups_base = ou=Group,dc=csclub,dc=internal -- 2.39.2 From 25b7412366bbb2823b0557e11a9bcf7c74a28577 Mon Sep 17 00:00:00 2001 From: Max Erenberg <> Date: Sat, 11 Sep 2021 13:28:15 -0400 Subject: [PATCH 8/8] fix tests --- tests/ceo/cli/test_db_mysql.py | 30 ++++++++++++++++------------- tests/ceo/cli/test_db_postgresql.py | 26 ++++++++++++++----------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/tests/ceo/cli/test_db_mysql.py b/tests/ceo/cli/test_db_mysql.py index 699c6a7..6418b0c 100644 --- a/tests/ceo/cli/test_db_mysql.py +++ b/tests/ceo/cli/test_db_mysql.py @@ -1,11 +1,11 @@ -import pytest import os from click.testing import CliRunner -from ceo.cli import cli - from mysql.connector import connect from mysql.connector.errors import ProgrammingError +import pytest + +from ceo.cli import cli def mysql_attempt_connection(host, username, password): @@ -26,12 +26,13 @@ def test_mysql(cli_setup, cfg, ldap_user): runner = CliRunner() username = ldap_user.uid + os.makedirs(ldap_user.home_directory) host = cfg.get("mysql_host") info_file_path = os.path.join(ldap_user.home_directory, "ceo-mysql-info") assert not os.path.isfile(info_file_path) # create database for user - result = runner.invoke(cli, ['mysql', 'create', username]) + result = runner.invoke(cli, ['mysql', 'create', username], input='y\n') assert result.exit_code == 0 assert os.path.isfile(info_file_path) @@ -40,22 +41,24 @@ def test_mysql(cli_setup, cfg, ldap_user): with open(info_file_path, 'r') as file: old_info = file.read() - expected = f"""MySQL database created + expected = f"""Are you sure you want to create a MySQL database for {username}? [y/N]: y +MySQL database created. +Connection Information: - Connection Information: +Database: {username} +Username: {username} +Password: {passwd} +Host: {host} - Database: {username} - Username: {username} - Password: {passwd} - Host: {host} - - Settings and more info has been written to {info_file_path}""" +These settings have been written to {info_file_path}. +""" assert result.output == expected mysql_attempt_connection(host, username, passwd) # perform password reset for user - result = runner.invoke(cli, ['mysql', 'pwreset', username], input="y\n") + # confirm once to reset password, another to overwrite the file + result = runner.invoke(cli, ['mysql', 'pwreset', username], input="y\ny\n") assert result.exit_code == 0 response_arr = result.output.split() @@ -76,3 +79,4 @@ def test_mysql(cli_setup, cfg, ldap_user): mysql_attempt_connection(host, username, passwd) os.remove(info_file_path) + os.rmdir(ldap_user.home_directory) diff --git a/tests/ceo/cli/test_db_postgresql.py b/tests/ceo/cli/test_db_postgresql.py index f79d88d..ddd2353 100644 --- a/tests/ceo/cli/test_db_postgresql.py +++ b/tests/ceo/cli/test_db_postgresql.py @@ -28,12 +28,13 @@ def test_postgresql(cli_setup, cfg, ldap_user): runner = CliRunner() username = ldap_user.uid + os.makedirs(ldap_user.home_directory) host = cfg.get("postgresql_host") - info_file_path = os.path.join(ldap_user.home_directory, "ceo-psql-info") + info_file_path = os.path.join(ldap_user.home_directory, "ceo-postgresql-info") assert not os.path.isfile(info_file_path) # create database for user - result = runner.invoke(cli, ['postgresql', 'create', username]) + result = runner.invoke(cli, ['postgresql', 'create', username], input='y\n') assert result.exit_code == 0 assert os.path.isfile(info_file_path) @@ -42,22 +43,24 @@ def test_postgresql(cli_setup, cfg, ldap_user): with open(info_file_path, 'r') as file: old_info = file.read() - expected = f"""PostgreSQL database created + expected = f"""Are you sure you want to create a PostgreSQL database for {username}? [y/N]: y +PostgreSQL database created. +Connection Information: - Connection Information: +Database: {username} +Username: {username} +Password: {passwd} +Host: {host} - Database: {username} - Username: {username} - Password: {passwd} - Host: {host} - - Settings and more info has been written to {info_file_path}""" +These settings have been written to {info_file_path}. +""" assert result.output == expected psql_attempt_connection(host, username, passwd) # perform password reset for user - result = runner.invoke(cli, ['postgresql', 'pwreset', username], input="y\n") + # confirm once to reset password, another to overwrite the file + result = runner.invoke(cli, ['postgresql', 'pwreset', username], input="y\ny\n") assert result.exit_code == 0 response_arr = result.output.split() @@ -78,3 +81,4 @@ def test_postgresql(cli_setup, cfg, ldap_user): psql_attempt_connection(host, username, passwd) os.remove(info_file_path) + os.rmdir(ldap_user.home_directory) -- 2.39.2