#!/usr/bin/env python3 """ Co-routines for handling various forms in Buckler. """ from passlib.hash import argon2 import tools async def invite_user(request): """Allows an admin to invite a new user.""" if not request['session']['admin']: return {'main': "You do not have permission to do that."} data = await request.post() email = data.get('email') if not email: return {'invite_user': "This field is required."} # TODO: validate email address await tools.send_invite(request, email) return {} async def change_user_perms(request): """Allows an admin to change user permissions.""" data = await request.post() pluses = [] for key, value in data.items(): if key == 'form_name': continue username, _, app_name = key.partition('-') pluses.append((username, app_name)) async with request.app['pool'].acquire() as conn: apps = await conn.fetch( "SELECT id, name FROM app_info") users = {p[0] for p in pluses} minuses = [] for user in users: for app in apps: minuses.append((user, app['name'])) minuses = [m for m in minuses if m not in pluses] await conn.executemany( "INSERT INTO app_user (user_id, app_id) " "VALUES ((SELECT id FROM user_info WHERE username = $1), " "(SELECT id FROM app_info WHERE name = $2)) " "ON CONFLICT ON CONSTRAINT app_user_pkey DO NOTHING", pluses) await conn.executemany( "DELETE FROM app_user " "WHERE user_id = (SELECT id FROM user_info WHERE username = $1) " "AND app_id = (SELECT id FROM app_info WHERE name = $2)", minuses) return {} async def new_app(request): """Allows an admin to add a new app to be managed by Buckler.""" data = await request.post() app_name = data.get('app_name') app_url = data.get('app_url') app_key = data.get('app_key') key_hash = argon2.hash(app_key) async with request.app['pool'].acquire() as conn: await conn.execute( "INSERT INTO app_info (name, url, key_hash) " "VALUES ($1, $2, $3)", app_name, app_url, key_hash) return {} async def change_password(request): """Allows a user to change their password.""" errors = {} data = await request.post() current_pw = data.get('current_password') new_pw = data.get('new_password') verify_pw = data.get('verify_password') if not all((current_pw, new_pw, verify_pw)): errors['change_password'] = "All fields are required." return errors if not new_pw == verify_pw: errors['change_password'] = "Passwords do not match." return errors async with request.app['pool'].acquire() as conn: pw_hash = await conn.fetchrow( "SELECT password_hash FROM user_info WHERE id = $1", request['session']['id']) if not argon2.verify(current_pw, pw_hash['password_hash']): errors['change_password'] = "Invalid password." return errors h = argon2.hash(new_pw) await conn.execute( "UPDATE user_info SET password_hash = $1 WHERE id = $2", h, request['session']['id']) return errors async def delete_key(request): """Allows a user to delete a security key.""" data = await request.post() async with request.app['pool'].acquire() as conn: for key, value in data.items(): key_id = key.replace('fido-', '') if not key_id: continue try: key_id = int(key_id) except ValueError: continue if value != 'on': continue await conn.execute( "DELETE FROM user_credential " "WHERE id = $1 AND user_id = $2", key_id, request['session']['id']) return {} async def delete_session(request): """Allows a user to delete a session .""" data = await request.post() async with request.app['pool'].acquire() as conn: for key, value in data.items(): session_id = key.replace('session-', '', 1) if not session_id: continue if value != 'on': continue await conn.execute( "DELETE FROM user_session " "WHERE id = $1 AND user_id = $2", session_id, request['session']['id']) return {}