use unix_socket auth for MySQL

This commit is contained in:
Max Erenberg 2021-09-11 12:52:33 -04:00
parent 0d94b1fafe
commit 1cd07c228c
9 changed files with 140 additions and 162 deletions

View File

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

View File

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

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

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

View File

@ -1,49 +1,6 @@
import click import click
import os
from zope import component from .database import create as db_create, pwreset as db_pwreset, delete as db_delete
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}""")
@click.group(short_help='Perform operations on MySQL') @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') @mysql.command(short_help='Create a MySQL database for a user')
@click.argument('username') @click.argument('username')
def create(username): def create(username):
resp = http_get(f'/api/members/{username}') db_create(username, 'mysql')
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)
@mysql.command(short_help='Reset the password of a MySQL user') @mysql.command(short_help='Reset the password of a MySQL user')
@click.argument('username') @click.argument('username')
def pwreset(username): def pwreset(username):
resp = http_get(f'/api/members/{username}') db_pwreset(username, 'mysql')
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)
@mysql.command(short_help="Delete the database of a MySQL user") @mysql.command(short_help="Delete the database of a MySQL user")
@click.argument('username') @click.argument('username')
def delete(username): def delete(username):
check_if_in_development() db_delete(username, 'mysql')
click.confirm("Are you sure?", abort=True)
resp = http_delete(f'/api/db/mysql/{username}')
handle_sync_response(resp)

View File

@ -1,49 +1,6 @@
import click import click
import os
from zope import component from .database import create as db_create, pwreset as db_pwreset, delete as db_delete
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}""")
@click.group(short_help='Perform operations on PostgreSQL') @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') @postgresql.command(short_help='Create a PostgreSQL database for a user')
@click.argument('username') @click.argument('username')
def create(username): def create(username):
resp = http_get(f'/api/members/{username}') db_create(username, 'postgresql')
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)
@postgresql.command(short_help='Reset the password of a PostgreSQL user') @postgresql.command(short_help='Reset the password of a PostgreSQL user')
@click.argument('username') @click.argument('username')
def pwreset(username): def pwreset(username):
resp = http_get(f'/api/members/{username}') db_pwreset(username, 'postgresql')
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)
@postgresql.command(short_help="Delete the database of a PostgreSQL user") @postgresql.command(short_help="Delete the database of a PostgreSQL user")
@click.argument('username') @click.argument('username')
def delete(username): def delete(username):
check_if_in_development() db_delete(username, 'postgresql')
click.confirm("Are you sure?", abort=True)
resp = http_delete(f'/api/db/postgresql/{username}')
handle_sync_response(resp)

View File

@ -49,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:
cursor.execute(cmd, {'password': password})
if response_is_empty(search_for_db, con):
cursor.execute(create_database)
else:
raise UserAlreadyExistsError() 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 return password
def reset_db_passwd(self, username: str) -> str: def reset_db_passwd(self, username: str) -> str:
@ -78,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):

View File

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

View File

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

View File

@ -13,7 +13,7 @@ port = 9987
[ldap] [ldap]
admin_principal = ceod/admin admin_principal = ceod/admin
server_url = ldap://ldap-master.csclub.internal server_url = ldap://auth1.csclub.internal
sasl_realm = CSCLUB.INTERNAL sasl_realm = CSCLUB.INTERNAL
users_base = ou=People,dc=csclub,dc=internal users_base = ou=People,dc=csclub,dc=internal
groups_base = ou=Group,dc=csclub,dc=internal groups_base = ou=Group,dc=csclub,dc=internal