From ccd44a063b83d80d36533d200339c6559e48c3a3 Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Sat, 4 Sep 2021 06:19:57 +0000 Subject: [PATCH] Add Discourse set password integration --- authserver/auth_functions.py | 137 +++++++++++++++++++++++++++++++++- authserver/requirements.txt | 6 ++ authserver/secrets.py.example | 13 ++++ authserver/server.py | 14 +++- 4 files changed, 168 insertions(+), 2 deletions(-) diff --git a/authserver/auth_functions.py b/authserver/auth_functions.py index 9f89b65..684dd6e 100644 --- a/authserver/auth_functions.py +++ b/authserver/auth_functions.py @@ -2,15 +2,23 @@ from log import logger import time import secrets import subprocess +import requests +from uuid import uuid4 from flask import abort HTTP_NOTFOUND = 404 +random_email = lambda: 'spaceport-' + str(uuid4()).split('-')[0] + '@protospace.ca' + def set_wiki_password(username, password): # sets a user's wiki password # creates the account if it doesn't exist + if not secrets.WIKI_MAINTENANCE: + logger.error('Wiki setting not configured, aborting') + abort(400) + if not username: logger.error('Empty username, aborting') abort(400) @@ -34,6 +42,133 @@ def set_wiki_password(username, password): if result.stderr: abort(400) +def discourse_api_get(url, params={}): + headers = { + 'Api-Key': secrets.DISCOURSE_API_KEY, + 'Api-Username': secrets.DISCOURSE_API_USER, + } + response = requests.get(url, headers=headers, params=params, timeout=10) + response.raise_for_status() + logger.info('Response: %s %s', response.status_code, response.text) + return response + +def discourse_api_put(url, data={}): + headers = { + 'Api-Key': secrets.DISCOURSE_API_KEY, + 'Api-Username': secrets.DISCOURSE_API_USER, + } + response = requests.put(url, headers=headers, data=data, timeout=10) + response.raise_for_status() + logger.info('Response: %s %s', response.status_code, response.text) + return response + +def discourse_api_post(url, data={}): + headers = { + 'Api-Key': secrets.DISCOURSE_API_KEY, + 'Api-Username': secrets.DISCOURSE_API_USER, + } + response = requests.post(url, headers=headers, data=data, timeout=10) + response.raise_for_status() + logger.info('Response: %s %s', response.status_code, response.text) + return response + +def discourse_rails_script(script): + result = subprocess.run(['docker', 'exec', '-i', secrets.DISCOURSE_CONTAINER, 'rails', 'runner', script], + shell=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output = result.stdout or result.stderr + output = output.strip() or 'No complaints' + return result, output + +def set_discourse_password(username, password, first_name, email): + # sets a user's discourse password + # creates the account if it doesn't exist + # things to test: + # - user changes Spaceport password + # - user changes Spaceport password to same + # - new Spaceport signup + # - existing Discourse user Spaceport signup + # - existing Discourse user Spaceport signup with same email + # note: Spaceport emails are unconfirmed!! + + if not secrets.DISCOURSE_CONTAINER or not secrets.DISCOURSE_API_KEY or not secrets.DISCOURSE_API_USER: + logger.error('Discourse setting not configured, aborting') + abort(400) + + if not username: + logger.error('Empty username, aborting') + abort(400) + + if not password: + logger.error('Empty password, aborting') + abort(400) + + if not first_name: + logger.error('Empty first_name, aborting') + abort(400) + + if not email: + logger.error('Empty email, aborting') + abort(400) + + logger.info('Checking Discourse for existing email: ' + email) + params = { + 'filter': email, + 'show_emails': 'true', + } + response = discourse_api_get('https://forum.protospace.ca/admin/users/list/active.json', params) + response = response.json() + + for user in response: + if user['email'] == email: + if user['username'] == username: + logger.info('Username match, skipping') + continue + + new_email = random_email() + logger.info('Email found on different user %s, changing to: %s', user['username'], new_email) + + script = 'UserEmail.find_by(email: "{}").update!(email: "{}")'.format(email, new_email) + result, output = discourse_rails_script(script) + + logger.info('Confirming email change...') + response = discourse_api_get('https://forum.protospace.ca/admin/users/list/active.json', params) + if len(response.json()): + logger.error('Email change failed, aborting') + abort(400) + + + logger.info('Creating Discourse user for: ' + username) + + data = { + 'name': first_name, + 'username': username, + 'password': password, + 'email': email, + 'active': True, + 'approved': True, + } + response = discourse_api_post('https://forum.protospace.ca/users.json', data) + response = response.json() + + if response['success']: + logger.info('Skipping set password') + return True + + logger.info('User exists, setting Discourse password for: ' + username) + + script = 'User.find_by(username: "{}").update!(password: "{}")'.format(username, password) + result, output = discourse_rails_script(script) + + if 'Password is the same' in result.stderr: + logger.info('Output: Password is the same as your current password. (ActiveRecord::RecordInvalid)') + return True + else: + logger.info('Output: ' + output) + + if result.stderr: + abort(400) + if __name__ == '__main__': - set_wiki_password('tanner.collin', 'protospace1') + #set_wiki_password('tanner.collin', 'protospace1') + set_discourse_password('test8a', 'protospace1', 'testie', 'test8@example.com') pass diff --git a/authserver/requirements.txt b/authserver/requirements.txt index e4c5de6..479e5ad 100644 --- a/authserver/requirements.txt +++ b/authserver/requirements.txt @@ -1,6 +1,12 @@ +certifi==2021.5.30 +charset-normalizer==2.0.4 click==7.1.2 Flask==1.1.2 +idna==3.2 itsdangerous==1.1.0 Jinja2==2.11.3 +logging-tree==1.9 MarkupSafe==1.1.1 +requests==2.26.0 +urllib3==1.26.6 Werkzeug==1.0.1 diff --git a/authserver/secrets.py.example b/authserver/secrets.py.example index 9a8f8aa..83ba42a 100644 --- a/authserver/secrets.py.example +++ b/authserver/secrets.py.example @@ -10,3 +10,16 @@ AUTH_TOKEN = '' # Probably: # /var/www/wiki/maintenance WIKI_MAINTENANCE = '' + +# The ID of the Docker container. +# Find it with docker ps +# Probably something like: +# 9c81ac530cdd +DISCOURSE_CONTAINER = '' + +# API key created here: +# https://forum.protospace.ca/admin/api/keys +DISCOURSE_API_KEY = '' + +# Username who created the API key +DISCOURSE_API_USER = '' diff --git a/authserver/server.py b/authserver/server.py index 5924f4a..4d43d1f 100644 --- a/authserver/server.py +++ b/authserver/server.py @@ -23,7 +23,7 @@ def index(): def ping(): return 'pong' -@app.route('/set-password', methods=['POST']) +@app.route('/set-wiki-password', methods=['POST']) def set_password(): check_auth() @@ -33,5 +33,17 @@ def set_password(): auth_functions.set_wiki_password(username, password) return '' +@app.route('/set-discourse-password', methods=['POST']) +def set_password(): + check_auth() + + username = request.form['username'] + password = request.form['password'] + first_name = request.form['first_name'] + email = request.form['email'] + + auth_functions.set_discourse_password(username, password, first_name, email) + return '' + if __name__ == '__main__': app.run(debug=True, host='0.0.0.0')