Merge branch 'v1' into docker
continuous-integration/drone/pr Build is passing
Details
continuous-integration/drone/pr Build is passing
Details
This commit is contained in:
commit
0d408e4ed3
|
@ -33,7 +33,7 @@ host all postgres 0.0.0.0/0 md5
|
||||||
local all all peer
|
local all all peer
|
||||||
host all all localhost md5
|
host all all localhost md5
|
||||||
|
|
||||||
local sameuser all md5
|
local sameuser all peer
|
||||||
host sameuser all 0.0.0.0/0 md5
|
host sameuser all 0.0.0.0/0 md5
|
||||||
EOF
|
EOF
|
||||||
grep -Eq "^listen_addresses = '*'$" $POSTGRES_DIR/postgresql.conf || \
|
grep -Eq "^listen_addresses = '*'$" $POSTGRES_DIR/postgresql.conf || \
|
||||||
|
|
|
@ -122,7 +122,7 @@ host all postgres 0.0.0.0/0 md5
|
||||||
local all all peer
|
local all all peer
|
||||||
host all all localhost md5
|
host all all localhost md5
|
||||||
|
|
||||||
local sameuser all md5
|
local sameuser all peer
|
||||||
host sameuser all 0.0.0.0/0 md5
|
host sameuser all 0.0.0.0/0 md5
|
||||||
```
|
```
|
||||||
**Warning**: in prod, the postgres user should only be allowed to connect locally,
|
**Warning**: in prod, the postgres user should only be allowed to connect locally,
|
||||||
|
|
|
@ -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)
|
|
@ -4,6 +4,8 @@ from .members import members
|
||||||
from .groups import groups
|
from .groups import groups
|
||||||
from .positions import positions
|
from .positions import positions
|
||||||
from .updateprograms import updateprograms
|
from .updateprograms import updateprograms
|
||||||
|
from .mysql import mysql
|
||||||
|
from .postgresql import postgresql
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
|
@ -15,3 +17,5 @@ cli.add_command(members)
|
||||||
cli.add_command(groups)
|
cli.add_command(groups)
|
||||||
cli.add_command(positions)
|
cli.add_command(positions)
|
||||||
cli.add_command(updateprograms)
|
cli.add_command(updateprograms)
|
||||||
|
cli.add_command(mysql)
|
||||||
|
cli.add_command(postgresql)
|
||||||
|
|
|
@ -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')
|
|
@ -1,4 +1,5 @@
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from typing import List, Tuple, Dict
|
from typing import List, Tuple, Dict
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
|
@ -110,7 +110,10 @@ class CeoFrame(Frame):
|
||||||
layout.add_widget(Divider())
|
layout.add_widget(Divider())
|
||||||
|
|
||||||
def _back():
|
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():
|
def _next():
|
||||||
if on_next_excl is not None:
|
if on_next_excl is not None:
|
||||||
|
|
|
@ -61,6 +61,12 @@ class Model:
|
||||||
'uid': '',
|
'uid': '',
|
||||||
'unsubscribe': True,
|
'unsubscribe': True,
|
||||||
},
|
},
|
||||||
|
'CreateDatabase': {
|
||||||
|
'uid': '',
|
||||||
|
},
|
||||||
|
'ResetDatabasePassword': {
|
||||||
|
'uid': '',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
self.viewdata = deepcopy(self._initial_viewdata)
|
self.viewdata = deepcopy(self._initial_viewdata)
|
||||||
# data which is shared between multiple views
|
# data which is shared between multiple views
|
||||||
|
@ -69,6 +75,8 @@ class Model:
|
||||||
self.operations = None
|
self.operations = None
|
||||||
self.deferred_req = None
|
self.deferred_req = None
|
||||||
self.resp = None
|
self.resp = None
|
||||||
|
self.db_type = None
|
||||||
|
self.user_dict = None
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.viewdata = deepcopy(self._initial_viewdata)
|
self.viewdata = deepcopy(self._initial_viewdata)
|
||||||
|
@ -77,6 +85,8 @@ class Model:
|
||||||
self.operations = None
|
self.operations = None
|
||||||
self.deferred_req = None
|
self.deferred_req = None
|
||||||
self.resp = None
|
self.resp = None
|
||||||
|
self.db_type = None
|
||||||
|
self.user_dict = None
|
||||||
self.title = None
|
self.title = None
|
||||||
self.error_message = None
|
self.error_message = None
|
||||||
self.scene_stack.clear()
|
self.scene_stack.clear()
|
||||||
|
|
|
@ -44,7 +44,7 @@ class ResultView(CeoFrame):
|
||||||
def _resultview_on_load(self):
|
def _resultview_on_load(self):
|
||||||
self._add_text()
|
self._add_text()
|
||||||
resp = self._model.resp
|
resp = self._model.resp
|
||||||
if resp.status_code != 200:
|
if not resp.ok:
|
||||||
self._add_text('An error occurred:')
|
self._add_text('An error occurred:')
|
||||||
if resp.headers.get('content-type') == 'application/json':
|
if resp.headers.get('content-type') == 'application/json':
|
||||||
err_msg = resp.json()['error']
|
err_msg = resp.json()['error']
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import functools
|
|
||||||
|
|
||||||
from asciimatics.widgets import ListBox, Layout, Divider, Button, Label
|
from asciimatics.widgets import ListBox, Layout, Divider, Button, Label
|
||||||
from asciimatics.exceptions import NextScene, StopApplication
|
from asciimatics.exceptions import NextScene, StopApplication
|
||||||
|
|
||||||
|
@ -23,40 +21,50 @@ class WelcomeView(CeoFrame):
|
||||||
('Change login shell', 'ChangeLoginShell'),
|
('Change login shell', 'ChangeLoginShell'),
|
||||||
('Set forwarding addresses', 'SetForwardingAddresses'),
|
('Set forwarding addresses', 'SetForwardingAddresses'),
|
||||||
]
|
]
|
||||||
members_menu = self._create_menu(
|
|
||||||
members_menu_items, 'members', self._members_menu_select)
|
|
||||||
groups_menu_items = [
|
groups_menu_items = [
|
||||||
('Add group', 'AddGroup'),
|
('Add group', 'AddGroup'),
|
||||||
('Get group members', 'GetGroup'),
|
('Get group members', 'GetGroup'),
|
||||||
('Add member to group', 'AddMemberToGroup'),
|
('Add member to group', 'AddMemberToGroup'),
|
||||||
('Remove member from group', 'RemoveMemberFromGroup'),
|
('Remove member from group', 'RemoveMemberFromGroup'),
|
||||||
]
|
]
|
||||||
groups_menu = self._create_menu(groups_menu_items, 'groups')
|
|
||||||
db_menu_items = [
|
db_menu_items = [
|
||||||
('Create MySQL database', 'CreateMySQL'),
|
('Create MySQL database', 'CreateDatabase'),
|
||||||
('Reset MySQL password', 'ResetMySQLPassword'),
|
('Reset MySQL password', 'ResetDatabasePassword'),
|
||||||
('Create PostgreSQL database', 'CreatePostgreSQL'),
|
('Create PostgreSQL database', 'CreateDatabase'),
|
||||||
('Reset PostgreSQL password', 'ResetPostgreSQLPassword'),
|
('Reset PostgreSQL password', 'ResetDatabasePassword'),
|
||||||
]
|
]
|
||||||
db_menu = self._create_menu(
|
|
||||||
db_menu_items, 'databases', self._db_menu_select)
|
|
||||||
positions_menu_items = [
|
positions_menu_items = [
|
||||||
('Get positions', 'GetPositions'),
|
('Get positions', 'GetPositions'),
|
||||||
('Set positions', 'SetPositions'),
|
('Set positions', 'SetPositions'),
|
||||||
]
|
]
|
||||||
positions_menu = self._create_menu(positions_menu_items, 'positions')
|
self.menu_items = [
|
||||||
self._menu_groups = {
|
('members', members_menu_items),
|
||||||
'members': members_menu_items,
|
('groups', groups_menu_items),
|
||||||
'groups': groups_menu_items,
|
('databases', db_menu_items),
|
||||||
'databases': db_menu_items,
|
('positions', positions_menu_items),
|
||||||
'positions': positions_menu_items,
|
]
|
||||||
}
|
self.menu_items_dict = dict(self.menu_items)
|
||||||
layout = Layout([1, 4, 1], fill_frame=True)
|
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)
|
self.add_layout(layout)
|
||||||
layout.add_widget(members_menu, 1)
|
layout.add_widget(menu, 2)
|
||||||
layout.add_widget(groups_menu, 1)
|
for label in labels:
|
||||||
layout.add_widget(db_menu, 1)
|
layout.add_widget(label, 0)
|
||||||
layout.add_widget(positions_menu, 1)
|
|
||||||
|
|
||||||
layout = Layout([100])
|
layout = Layout([100])
|
||||||
self.add_layout(layout)
|
self.add_layout(layout)
|
||||||
|
@ -68,37 +76,25 @@ class WelcomeView(CeoFrame):
|
||||||
layout.add_widget(Button("Quit", self._quit), 2)
|
layout.add_widget(Button("Quit", self._quit), 2)
|
||||||
self.fix()
|
self.fix()
|
||||||
|
|
||||||
def _create_menu(self, menu_items, name, on_select=None):
|
def _menu_select(self):
|
||||||
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):
|
|
||||||
self.save()
|
self.save()
|
||||||
item_id = self.data[menu_name]
|
item_id = self.data['menu']
|
||||||
menu_items = self._menu_groups[menu_name]
|
# find which submenu the item belongs to
|
||||||
return menu_items[item_id]
|
counter = 0
|
||||||
|
for name, items in self.menu_items:
|
||||||
def _members_menu_select(self):
|
if item_id < counter + len(items):
|
||||||
desc, view = self._get_menu_item_desc_view('members')
|
break
|
||||||
|
counter += len(items)
|
||||||
|
submenu_idx = item_id - counter
|
||||||
|
desc, view = items[submenu_idx]
|
||||||
|
if name == 'members':
|
||||||
if desc.endswith('club rep'):
|
if desc.endswith('club rep'):
|
||||||
self._model.is_club_rep = True
|
self._model.is_club_rep = True
|
||||||
self._welcomeview_go_to_next_scene(desc, view)
|
elif name == 'databases':
|
||||||
|
if 'MySQL' in desc:
|
||||||
def _db_menu_select(self):
|
self._model.db_type = 'mysql'
|
||||||
pass
|
else:
|
||||||
|
self._model.db_type = 'postgresql'
|
||||||
def _generic_menu_select(self, menu_name):
|
|
||||||
desc, view = self._get_menu_item_desc_view('groups')
|
|
||||||
self._welcomeview_go_to_next_scene(desc, view)
|
self._welcomeview_go_to_next_scene(desc, view)
|
||||||
|
|
||||||
def _welcomeview_go_to_next_scene(self, desc, view):
|
def _welcomeview_go_to_next_scene(self, desc, view):
|
||||||
|
|
|
@ -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 @@
|
||||||
|
|
|
@ -10,6 +10,10 @@ from .Model import Model
|
||||||
from .ResultView import ResultView
|
from .ResultView import ResultView
|
||||||
from .TransactionView import TransactionView
|
from .TransactionView import TransactionView
|
||||||
from .WelcomeView import WelcomeView
|
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.AddGroupView import AddGroupView
|
||||||
from .groups.AddMemberToGroupView import AddMemberToGroupView
|
from .groups.AddMemberToGroupView import AddMemberToGroupView
|
||||||
from .groups.GetGroupView import GetGroupView
|
from .groups.GetGroupView import GetGroupView
|
||||||
|
@ -56,6 +60,10 @@ def screen_wrapper(screen, last_scene, model):
|
||||||
('GetGroupResult', GetGroupResultView(screen, width, height, model)),
|
('GetGroupResult', GetGroupResultView(screen, width, height, model)),
|
||||||
('AddMemberToGroup', AddMemberToGroupView(screen, width, height, model)),
|
('AddMemberToGroup', AddMemberToGroupView(screen, width, height, model)),
|
||||||
('RemoveMemberFromGroup', RemoveMemberFromGroupView(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 = [
|
scenes = [
|
||||||
Scene([view], -1, name=name) for name, view in views
|
Scene([view], -1, name=name) for name, view in views
|
||||||
|
|
48
ceo/utils.py
48
ceo/utils.py
|
@ -1,5 +1,6 @@
|
||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from typing import List, Dict, Tuple, Callable
|
from typing import List, Dict, Tuple, Callable
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
@ -193,3 +194,50 @@ def defer(f: Callable, *args, **kwargs):
|
||||||
def wrapper():
|
def wrapper():
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return wrapper
|
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
|
||||||
|
|
|
@ -31,9 +31,9 @@ def db_exception_handler(func):
|
||||||
except InvalidUsernameError:
|
except InvalidUsernameError:
|
||||||
return {'error': 'username contains invalid characters'}, 400
|
return {'error': 'username contains invalid characters'}, 400
|
||||||
except DatabaseConnectionError:
|
except DatabaseConnectionError:
|
||||||
return {'error': 'unable to connect or authenticate to sql server'}, 500
|
return {'error': 'unable to connect to sql server'}, 500
|
||||||
except DatabasePermissionError:
|
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
|
return function
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ from ceod.utils import gen_password
|
||||||
from ceod.db.utils import response_is_empty
|
from ceod.db.utils import response_is_empty
|
||||||
|
|
||||||
from mysql.connector import connect
|
from mysql.connector import connect
|
||||||
from mysql.connector.errors import InterfaceError, ProgrammingError
|
from mysql.connector.errors import OperationalError, ProgrammingError
|
||||||
|
|
||||||
logger = logger_factory(__name__)
|
logger = logger_factory(__name__)
|
||||||
|
|
||||||
|
@ -35,9 +35,11 @@ class MySQLService:
|
||||||
password=self.auth_password,
|
password=self.auth_password,
|
||||||
) as con:
|
) as con:
|
||||||
yield con
|
yield con
|
||||||
except InterfaceError as e:
|
# unable to connect
|
||||||
|
except OperationalError as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
raise DatabaseConnectionError()
|
raise DatabaseConnectionError()
|
||||||
|
# invalid credentials / user does not exist / invalid permissions for action
|
||||||
except ProgrammingError as e:
|
except ProgrammingError as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
raise DatabasePermissionError()
|
raise DatabasePermissionError()
|
||||||
|
@ -47,24 +49,21 @@ class MySQLService:
|
||||||
search_for_user = f"SELECT user FROM mysql.user WHERE user='{username}'"
|
search_for_user = f"SELECT user FROM mysql.user WHERE user='{username}'"
|
||||||
search_for_db = f"SHOW DATABASES LIKE '{username}'"
|
search_for_db = f"SHOW DATABASES LIKE '{username}'"
|
||||||
# CREATE USER can't be used in a query with multiple statements
|
# CREATE USER can't be used in a query with multiple statements
|
||||||
create_user_commands = [
|
create_local_user_cmd = f"CREATE USER '{username}'@'localhost' IDENTIFIED VIA unix_socket"
|
||||||
f"CREATE USER '{username}'@'localhost' IDENTIFIED BY %(password)s",
|
create_user_cmd = f"CREATE USER '{username}'@'%' IDENTIFIED BY %(password)s"
|
||||||
f"CREATE USER '{username}'@'%' IDENTIFIED BY %(password)s",
|
|
||||||
]
|
|
||||||
create_database = f"""
|
create_database = f"""
|
||||||
CREATE DATABASE {username};
|
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}'@'%';
|
GRANT ALL PRIVILEGES ON {username}.* TO '{username}'@'%';
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with self.mysql_connection() as con, con.cursor() as cursor:
|
with self.mysql_connection() as con, con.cursor() as cursor:
|
||||||
if response_is_empty(search_for_user, con):
|
if not response_is_empty(search_for_user, con):
|
||||||
for cmd in create_user_commands:
|
raise UserAlreadyExistsError()
|
||||||
cursor.execute(cmd, {'password': password})
|
cursor.execute(create_local_user_cmd)
|
||||||
|
cursor.execute(create_user_cmd, {'password': password})
|
||||||
if response_is_empty(search_for_db, con):
|
if response_is_empty(search_for_db, con):
|
||||||
cursor.execute(create_database)
|
cursor.execute(create_database)
|
||||||
else:
|
|
||||||
raise UserAlreadyExistsError()
|
|
||||||
return password
|
return password
|
||||||
|
|
||||||
def reset_db_passwd(self, username: str) -> str:
|
def reset_db_passwd(self, username: str) -> str:
|
||||||
|
@ -76,10 +75,9 @@ class MySQLService:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with self.mysql_connection() as con, con.cursor() as cursor:
|
with self.mysql_connection() as con, con.cursor() as cursor:
|
||||||
if not response_is_empty(search_for_user, con):
|
if response_is_empty(search_for_user, con):
|
||||||
cursor.execute(reset_password, {'password': password})
|
|
||||||
else:
|
|
||||||
raise UserNotFoundError(username)
|
raise UserNotFoundError(username)
|
||||||
|
cursor.execute(reset_password, {'password': password})
|
||||||
return password
|
return password
|
||||||
|
|
||||||
def delete_db(self, username: str):
|
def delete_db(self, username: str):
|
||||||
|
|
|
@ -39,9 +39,11 @@ class PostgreSQLService:
|
||||||
)
|
)
|
||||||
con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
|
con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
|
||||||
yield con
|
yield con
|
||||||
|
# unable to connect / invalid credentials / user does not exist
|
||||||
except OperationalError as e:
|
except OperationalError as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
raise DatabaseConnectionError()
|
raise DatabaseConnectionError()
|
||||||
|
# invalid permissions for action
|
||||||
except ProgrammingError as e:
|
except ProgrammingError as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
raise DatabasePermissionError()
|
raise DatabasePermissionError()
|
||||||
|
|
|
@ -56,7 +56,6 @@ class User:
|
||||||
|
|
||||||
self.ldap_srv = component.getUtility(ILDAPService)
|
self.ldap_srv = component.getUtility(ILDAPService)
|
||||||
self.krb_srv = component.getUtility(IKerberosService)
|
self.krb_srv = component.getUtility(IKerberosService)
|
||||||
self.file_srv = component.getUtility(IFileService)
|
|
||||||
|
|
||||||
def to_dict(self, get_forwarding_addresses: bool = False) -> Dict:
|
def to_dict(self, get_forwarding_addresses: bool = False) -> Dict:
|
||||||
data = {
|
data = {
|
||||||
|
@ -103,10 +102,12 @@ class User:
|
||||||
self.krb_srv.change_password(self.uid, password)
|
self.krb_srv.change_password(self.uid, password)
|
||||||
|
|
||||||
def create_home_dir(self):
|
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):
|
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):
|
def subscribe_to_mailing_list(self, mailing_list: str):
|
||||||
component.getUtility(IMailmanService).subscribe(self.uid, mailing_list)
|
component.getUtility(IMailmanService).subscribe(self.uid, mailing_list)
|
||||||
|
@ -163,7 +164,9 @@ class User:
|
||||||
self.positions = positions
|
self.positions = positions
|
||||||
|
|
||||||
def get_forwarding_addresses(self) -> List[str]:
|
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]):
|
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)
|
||||||
|
|
|
@ -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)
|
|
@ -5,6 +5,7 @@ uw_domain = uwaterloo.internal
|
||||||
[ceod]
|
[ceod]
|
||||||
# this is the host with the ceod/admin Kerberos key
|
# this is the host with the ceod/admin Kerberos key
|
||||||
admin_host = phosphoric-acid
|
admin_host = phosphoric-acid
|
||||||
|
database_host = coffee
|
||||||
use_https = false
|
use_https = false
|
||||||
port = 9987
|
port = 9987
|
||||||
|
|
||||||
|
@ -12,3 +13,9 @@ port = 9987
|
||||||
required = president,vice-president,sysadmin
|
required = president,vice-president,sysadmin
|
||||||
available = president,vice-president,treasurer,secretary,
|
available = president,vice-president,treasurer,secretary,
|
||||||
sysadmin,cro,librarian,imapd,webmaster,offsck
|
sysadmin,cro,librarian,imapd,webmaster,offsck
|
||||||
|
|
||||||
|
[mysql]
|
||||||
|
host = coffee
|
||||||
|
|
||||||
|
[postgresql]
|
||||||
|
host = coffee
|
||||||
|
|
Loading…
Reference in New Issue