commit
0d408e4ed3
@ -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) |
@ -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') |
@ -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') |
@ -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}.") |
@ -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' |
@ -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}.") |
@ -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' |
@ -0,0 +1 @@ |
||||
|
@ -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) |
@ -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) |
Loading…
Reference in new issue