Merge branch 'v1' into docker
continuous-integration/drone/pr Build is passing Details

This commit is contained in:
Max Erenberg 2021-09-17 22:31:55 -04:00
commit 0d408e4ed3
25 changed files with 599 additions and 80 deletions

View File

@ -33,7 +33,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 || \

View File

@ -122,7 +122,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,

70
ceo/cli/database.py Normal file
View File

@ -0,0 +1,70 @@
import os
from typing import Dict
import click
from zope import component
from ..utils import http_post, http_get, http_delete, write_db_creds
from .utils import handle_sync_response, 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')
if db_type == 'mysql':
db_type_name = 'MySQL'
else:
db_type_name = 'PostgreSQL'
wrote_to_file = write_db_creds(filename, user_dict, password, db_type, db_host)
if op == 'create':
click.echo(f'{db_type_name} database created.')
username = user_dict['uid']
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")
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")
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)

View File

@ -4,6 +4,8 @@ from .members import members
from .groups import groups
from .positions import positions
from .updateprograms import updateprograms
from .mysql import mysql
from .postgresql import postgresql
@click.group()
@ -15,3 +17,5 @@ cli.add_command(members)
cli.add_command(groups)
cli.add_command(positions)
cli.add_command(updateprograms)
cli.add_command(mysql)
cli.add_command(postgresql)

26
ceo/cli/mysql.py Normal file
View File

@ -0,0 +1,26 @@
import click
from .database import create as db_create, pwreset as db_pwreset, delete as db_delete
@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):
db_create(username, 'mysql')
@mysql.command(short_help='Reset the password of a MySQL user')
@click.argument('username')
def pwreset(username):
db_pwreset(username, 'mysql')
@mysql.command(short_help="Delete the database of a MySQL user")
@click.argument('username')
def delete(username):
db_delete(username, 'mysql')

26
ceo/cli/postgresql.py Normal file
View File

@ -0,0 +1,26 @@
import click
from .database import create as db_create, pwreset as db_pwreset, delete as db_delete
@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):
db_create(username, 'postgresql')
@postgresql.command(short_help='Reset the password of a PostgreSQL user')
@click.argument('username')
def pwreset(username):
db_pwreset(username, 'postgresql')
@postgresql.command(short_help="Delete the database of a PostgreSQL user")
@click.argument('username')
def delete(username):
db_delete(username, 'postgresql')

View File

@ -1,4 +1,5 @@
import socket
from typing import List, Tuple, Dict
import click

View File

@ -110,7 +110,10 @@ class CeoFrame(Frame):
layout.add_widget(Divider())
def _back():
raise NextScene(self._model.scene_stack.pop())
last_scene = self._model.scene_stack.pop()
if last_scene == 'Welcome':
self._model.reset()
raise NextScene(last_scene)
def _next():
if on_next_excl is not None:

View File

@ -61,6 +61,12 @@ class Model:
'uid': '',
'unsubscribe': True,
},
'CreateDatabase': {
'uid': '',
},
'ResetDatabasePassword': {
'uid': '',
},
}
self.viewdata = deepcopy(self._initial_viewdata)
# data which is shared between multiple views
@ -69,6 +75,8 @@ class Model:
self.operations = None
self.deferred_req = None
self.resp = None
self.db_type = None
self.user_dict = None
def reset(self):
self.viewdata = deepcopy(self._initial_viewdata)
@ -77,6 +85,8 @@ class Model:
self.operations = None
self.deferred_req = None
self.resp = None
self.db_type = None
self.user_dict = None
self.title = None
self.error_message = None
self.scene_stack.clear()

View File

@ -44,7 +44,7 @@ class ResultView(CeoFrame):
def _resultview_on_load(self):
self._add_text()
resp = self._model.resp
if resp.status_code != 200:
if not resp.ok:
self._add_text('An error occurred:')
if resp.headers.get('content-type') == 'application/json':
err_msg = resp.json()['error']

View File

@ -1,5 +1,3 @@
import functools
from asciimatics.widgets import ListBox, Layout, Divider, Button, Label
from asciimatics.exceptions import NextScene, StopApplication
@ -23,40 +21,50 @@ class WelcomeView(CeoFrame):
('Change login shell', 'ChangeLoginShell'),
('Set forwarding addresses', 'SetForwardingAddresses'),
]
members_menu = self._create_menu(
members_menu_items, 'members', self._members_menu_select)
groups_menu_items = [
('Add group', 'AddGroup'),
('Get group members', 'GetGroup'),
('Add member to group', 'AddMemberToGroup'),
('Remove member from group', 'RemoveMemberFromGroup'),
]
groups_menu = self._create_menu(groups_menu_items, 'groups')
db_menu_items = [
('Create MySQL database', 'CreateMySQL'),
('Reset MySQL password', 'ResetMySQLPassword'),
('Create PostgreSQL database', 'CreatePostgreSQL'),
('Reset PostgreSQL password', 'ResetPostgreSQLPassword'),
('Create MySQL database', 'CreateDatabase'),
('Reset MySQL password', 'ResetDatabasePassword'),
('Create PostgreSQL database', 'CreateDatabase'),
('Reset PostgreSQL password', 'ResetDatabasePassword'),
]
db_menu = self._create_menu(
db_menu_items, 'databases', self._db_menu_select)
positions_menu_items = [
('Get positions', 'GetPositions'),
('Set positions', 'SetPositions'),
]
positions_menu = self._create_menu(positions_menu_items, 'positions')
self._menu_groups = {
'members': members_menu_items,
'groups': groups_menu_items,
'databases': db_menu_items,
'positions': positions_menu_items,
}
layout = Layout([1, 4, 1], fill_frame=True)
self.menu_items = [
('members', members_menu_items),
('groups', groups_menu_items),
('databases', db_menu_items),
('positions', positions_menu_items),
]
self.menu_items_dict = dict(self.menu_items)
flat_menu_items = [item for name, items in self.menu_items for item in items]
menu = ListBox(
len(flat_menu_items),
[
(desc, i) for i, (desc, view) in
enumerate(flat_menu_items)
],
name='menu',
on_select=self._menu_select,
)
labels = []
for name, items in self.menu_items:
labels.append(Label(name.capitalize(), align='>'))
for _ in range(len(items) - 1):
labels.append(Label(''))
layout = Layout([5, 1, 8], fill_frame=True)
self.add_layout(layout)
layout.add_widget(members_menu, 1)
layout.add_widget(groups_menu, 1)
layout.add_widget(db_menu, 1)
layout.add_widget(positions_menu, 1)
layout.add_widget(menu, 2)
for label in labels:
layout.add_widget(label, 0)
layout = Layout([100])
self.add_layout(layout)
@ -68,37 +76,25 @@ class WelcomeView(CeoFrame):
layout.add_widget(Button("Quit", self._quit), 2)
self.fix()
def _create_menu(self, menu_items, name, on_select=None):
if on_select is None:
on_select = functools.partial(self._generic_menu_select, name)
return ListBox(
len(menu_items),
[
(desc, i) for i, (desc, view) in
enumerate(menu_items)
],
name=name,
label=name.capitalize(),
on_select=on_select,
)
def _get_menu_item_desc_view(self, menu_name: str):
def _menu_select(self):
self.save()
item_id = self.data[menu_name]
menu_items = self._menu_groups[menu_name]
return menu_items[item_id]
def _members_menu_select(self):
desc, view = self._get_menu_item_desc_view('members')
item_id = self.data['menu']
# find which submenu the item belongs to
counter = 0
for name, items in self.menu_items:
if item_id < counter + len(items):
break
counter += len(items)
submenu_idx = item_id - counter
desc, view = items[submenu_idx]
if name == 'members':
if desc.endswith('club rep'):
self._model.is_club_rep = True
self._welcomeview_go_to_next_scene(desc, view)
def _db_menu_select(self):
pass
def _generic_menu_select(self, menu_name):
desc, view = self._get_menu_item_desc_view('groups')
elif name == 'databases':
if 'MySQL' in desc:
self._model.db_type = 'mysql'
else:
self._model.db_type = 'postgresql'
self._welcomeview_go_to_next_scene(desc, view)
def _welcomeview_go_to_next_scene(self, desc, view):

View File

@ -0,0 +1,34 @@
import os
import requests
from zope import component
from ...utils import write_db_creds
from ..ResultView import ResultView
from ceo_common.interfaces import IConfig
class CreateDatabaseResultView(ResultView):
def show_result(self, resp: requests.Response):
password = resp.json()['password']
db_type = self._model.db_type
db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL'
db_host = component.getUtility(IConfig).get(f'{db_type}_host')
user_dict = self._model.user_dict
username = user_dict['uid']
filename = os.path.join(user_dict['home_directory'], f"ceo-{db_type}-info")
wrote_to_file = write_db_creds(
filename, user_dict, password, db_type, db_host)
self._add_text(f'{db_type_name} database created.', center=True)
self._add_text()
self._add_text((f'''Connection Information:
Database: {username}
Username: {username}
Password: {password}
Host: {db_host}'''))
self._add_text()
if wrote_to_file:
self._add_text(f"These settings have been written to {filename}.")
else:
self._add_text(f"We were unable to write these settings to {filename}.")

View File

@ -0,0 +1,44 @@
from asciimatics.widgets import Layout, Text
from ...utils import http_post, http_get, defer
from ..CeoFrame import CeoFrame
class CreateDatabaseView(CeoFrame):
def __init__(self, screen, width, height, model):
super().__init__(
screen, height, width, model, 'CreateDatabase',
save_data=True,
)
layout = Layout([100], fill_frame=True)
self.add_layout(layout)
self._username = Text("Username:", "uid")
layout.add_widget(self._username)
self.add_buttons(
back_btn=True, next_scene='Confirm',
on_next=self._next)
self.fix()
def _target(self):
username = self._username.value
db_type = self._model.db_type
resp = http_get(f'/api/members/{username}')
if not resp.ok:
return resp
user_dict = resp.json()
self._model.user_dict = user_dict
return http_post(f'/api/db/{db_type}/{username}')
def _next(self):
username = self._username.value
if not username:
return
if self._model.db_type == 'mysql':
db_type_name = 'MySQL'
else:
db_type_name = 'PostgreSQL'
self._model.confirm_lines = [
f'Are you sure you want to create a {db_type_name} database for {username}?',
]
self._model.deferred_req = defer(self._target)
self._model.result_view_name = 'CreateDatabaseResult'

View File

@ -0,0 +1,29 @@
import os
import requests
from zope import component
from ...utils import write_db_creds
from ..ResultView import ResultView
from ceo_common.interfaces import IConfig
class ResetDatabasePasswordResultView(ResultView):
def show_result(self, resp: requests.Response):
password = resp.json()['password']
db_type = self._model.db_type
db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL'
db_host = component.getUtility(IConfig).get(f'{db_type}_host')
user_dict = self._model.user_dict
username = user_dict['uid']
filename = os.path.join(user_dict['home_directory'], f"ceo-{db_type}-info")
wrote_to_file = write_db_creds(
filename, user_dict, password, db_type, db_host)
self._add_text(f'The new {db_type_name} password for {username} is:')
self._add_text()
self._add_text(password)
self._add_text()
if wrote_to_file:
self._add_text(f"The settings in {filename} have been updated.")
else:
self._add_text(f"We were unable to update the settings in {filename}.")

View File

@ -0,0 +1,44 @@
from asciimatics.widgets import Layout, Text
from ...utils import http_post, http_get, defer
from ..CeoFrame import CeoFrame
class ResetDatabasePasswordView(CeoFrame):
def __init__(self, screen, width, height, model):
super().__init__(
screen, height, width, model, 'ResetDatabasePassword',
save_data=True,
)
layout = Layout([100], fill_frame=True)
self.add_layout(layout)
self._username = Text("Username:", "uid")
layout.add_widget(self._username)
self.add_buttons(
back_btn=True, next_scene='Confirm',
on_next=self._next)
self.fix()
def _target(self):
username = self._username.value
db_type = self._model.db_type
resp = http_get(f'/api/members/{username}')
if not resp.ok:
return resp
user_dict = resp.json()
self._model.user_dict = user_dict
return http_post(f'/api/db/{db_type}/{username}/pwreset')
def _next(self):
username = self._username.value
if not username:
return
if self._model.db_type == 'mysql':
db_type_name = 'MySQL'
else:
db_type_name = 'PostgreSQL'
self._model.confirm_lines = [
f'Are you sure you want to reset the {db_type_name} password for {username}?',
]
self._model.deferred_req = defer(self._target)
self._model.result_view_name = 'ResetDatabasePasswordResult'

View File

@ -0,0 +1 @@

View File

@ -10,6 +10,10 @@ from .Model import Model
from .ResultView import ResultView
from .TransactionView import TransactionView
from .WelcomeView import WelcomeView
from .databases.CreateDatabaseView import CreateDatabaseView
from .databases.CreateDatabaseResultView import CreateDatabaseResultView
from .databases.ResetDatabasePasswordView import ResetDatabasePasswordView
from .databases.ResetDatabasePasswordResultView import ResetDatabasePasswordResultView
from .groups.AddGroupView import AddGroupView
from .groups.AddMemberToGroupView import AddMemberToGroupView
from .groups.GetGroupView import GetGroupView
@ -56,6 +60,10 @@ def screen_wrapper(screen, last_scene, model):
('GetGroupResult', GetGroupResultView(screen, width, height, model)),
('AddMemberToGroup', AddMemberToGroupView(screen, width, height, model)),
('RemoveMemberFromGroup', RemoveMemberFromGroupView(screen, width, height, model)),
('CreateDatabase', CreateDatabaseView(screen, width, height, model)),
('CreateDatabaseResult', CreateDatabaseResultView(screen, width, height, model)),
('ResetDatabasePassword', ResetDatabasePasswordView(screen, width, height, model)),
('ResetDatabasePasswordResult', ResetDatabasePasswordResultView(screen, width, height, model)),
]
scenes = [
Scene([view], -1, name=name) for name, view in views

View File

@ -1,5 +1,6 @@
import functools
import json
import os
from typing import List, Dict, Tuple, Callable
import requests
@ -193,3 +194,50 @@ def defer(f: Callable, *args, **kwargs):
def wrapper():
return f(*args, **kwargs)
return wrapper
def write_db_creds(
filename: str,
user_dict: Dict,
password: str,
db_type: str,
db_host: str,
) -> bool:
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'
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}
"""
try:
# TODO: use phosphoric-acid to write to file (phosphoric-acid makes
# internal API call to caffeine)
if os.path.isfile(filename):
os.rename(filename, filename + '.bak')
with open(filename, "w") as f:
f.write(info)
os.chown(filename, user_dict['uid_number'], user_dict['gid_number'])
os.chmod(filename, 0o640)
return True
except PermissionError:
return False

View File

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

View File

@ -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,9 +35,11 @@ class MySQLService:
password=self.auth_password,
) as con:
yield con
except InterfaceError as e:
# 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()
@ -47,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 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)
else:
raise UserAlreadyExistsError()
return password
def reset_db_passwd(self, username: str) -> str:
@ -76,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):

View File

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

View File

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

View File

@ -0,0 +1,82 @@
import os
import shutil
from click.testing import CliRunner
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):
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
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], input='y\n')
print(result.output)
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"""Are you sure you want to create a MySQL database for {username}? [y/N]: y
MySQL database created.
Connection Information:
Database: {username}
Username: {username}
Password: {passwd}
Host: {host}
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")
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)
shutil.rmtree(ldap_user.home_directory)

View File

@ -0,0 +1,83 @@
import pytest
import os
import shutil
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
os.makedirs(ldap_user.home_directory)
host = cfg.get("postgresql_host")
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], input='y\n')
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"""Are you sure you want to create a PostgreSQL database for {username}? [y/N]: y
PostgreSQL database created.
Connection Information:
Database: {username}
Username: {username}
Password: {passwd}
Host: {host}
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")
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)
shutil.rmtree(ldap_user.home_directory)

View File

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