From 422a8cc47489aa44b852ff0add1ef6ea63cfc1ff Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Thu, 3 Jan 2019 20:45:29 -0800 Subject: several auth improvements --- python/fatcat_web/__init__.py | 11 +++++ python/fatcat_web/auth.py | 60 ++++++++++++++++++++--- python/fatcat_web/routes.py | 45 ++++++++++++----- python/fatcat_web/templates/auth_login.html | 17 +++++++ python/fatcat_web/templates/auth_logout.html | 8 +++ python/fatcat_web/templates/auth_token_login.html | 29 +++++++++++ python/fatcat_web/templates/base.html | 15 ++++-- python/web_config.py | 9 +++- 8 files changed, 170 insertions(+), 24 deletions(-) create mode 100644 python/fatcat_web/templates/auth_login.html create mode 100644 python/fatcat_web/templates/auth_logout.html create mode 100644 python/fatcat_web/templates/auth_token_login.html diff --git a/python/fatcat_web/__init__.py b/python/fatcat_web/__init__.py index f8b72fd0..9cd5f812 100644 --- a/python/fatcat_web/__init__.py +++ b/python/fatcat_web/__init__.py @@ -26,6 +26,17 @@ conf = fatcat_client.Configuration() conf.host = "http://localhost:9411/v0" api = fatcat_client.DefaultApi(fatcat_client.ApiClient(conf)) +if Config.FATCAT_API_AUTH_TOKEN: + print("Found and using privileged token (eg, for account signup)") + priv_conf = fatcat_client.Configuration() + priv_conf.api_key["Authorization"] = Config.FATCAT_API_AUTH_TOKEN + priv_conf.api_key_prefix["Authorization"] = "Bearer" + priv_conf.host = 'http://localhost:9411/v0' + priv_api = fatcat_client.DefaultApi(fatcat_client.ApiClient(local_conf)) +else: + print("No privileged token found") + priv_api = None + from fatcat_web import routes, auth gitlab_bp = create_flask_blueprint(Gitlab, oauth, auth.handle_oauth) diff --git a/python/fatcat_web/auth.py b/python/fatcat_web/auth.py index f6672e87..385f5c49 100644 --- a/python/fatcat_web/auth.py +++ b/python/fatcat_web/auth.py @@ -1,27 +1,75 @@ from flask import Flask, render_template, send_from_directory, request, \ url_for, abort, g, redirect, jsonify, session -from fatcat_web import login_manager +from fatcat_web import login_manager, api +from flask_login import logout_user, login_user, UserMixin +import pymacaroons +def handle_logout(): + logout_user() + for k in ('editor', 'token'): + if k in session: + session.pop(k) + +def handle_token_login(token): + try: + m = pymacaroons.Macaroon.deserialize(token) + except pymacaroons.exceptions.MacaroonDeserializationException: + # TODO: what kind of Exceptions? + return abort(400) + # extract editor_id + editor_id = None + for caveat in m.first_party_caveats(): + caveat = caveat.caveat_id + if caveat.startswith(b"editor_id = "): + editor_id = caveat[12:].decode('utf-8') + if not editor_id: + abort(400) + # fetch editor info + editor = api.get_editor(editor_id).to_dict() + session['api_token'] = token + session['editor'] = editor + login_user(load_user(editor_id)) + return redirect("/") + # This will need to login/signup via fatcatd API, then set token in session def handle_oauth(remote, token, user_info): print(remote) if token: print(remote.name, token) if user_info: - # TODO: fetch api login/signup using user_info print(user_info) - # TODO: write token and username to session - # TODO: call login_user(load_user(editor_id)) + print(user_info.iss) + print(user_info.prefered_username) + + # fetch api login/signup using user_info + params = AuthOidc(remote.name, user_info.sub, user_info.iss) + resp = api.auth_oidc(params) + editor = resp['editor'] + api_token = resp['token'] + + # write token and username to session + session['api_token'] = api_token + session['editor'] = editor.editor_id + + # call login_user(load_user(editor_id)) + login_user(load_user(editor_id)) return redirect("/") + raise some_error @login_manager.user_loader def load_user(editor_id): - # NOTE: this should look for extra info in session, and update the user - # object with that. If session isn't loaded/valid, should return None + # looks for extra info in session, and updates the user object with that. + # If session isn't loaded/valid, should return None + if not 'editor' in session or not 'api_token' in session: + return None + editor = session['editor'] + token = session['api_token'] user = UserMixin() user.id = editor_id + user.username = editor['username'] + user.token = token return user diff --git a/python/fatcat_web/routes.py b/python/fatcat_web/routes.py index 51533a2f..5d46fe0b 100644 --- a/python/fatcat_web/routes.py +++ b/python/fatcat_web/routes.py @@ -4,6 +4,7 @@ import json from flask import Flask, render_template, send_from_directory, request, \ url_for, abort, g, redirect, jsonify, session from fatcat_web import app, api +from fatcat_web.auth import handle_token_login, handle_logout from fatcat_client.rest import ApiException from fatcat_web.search import do_search @@ -295,12 +296,6 @@ def work_view(ident): return render_template('deleted_entity.html', entity=entity) return render_template('work_view.html', work=entity, releases=releases) -@app.route('/editgroup/current', methods=['GET']) -def editgroup_current(): - raise NotImplementedError - #eg = api.get_or_create_editgroup() - #return redirect('/editgroup/{}'.format(eg.id)) - @app.route('/editgroup/', methods=['GET']) def editgroup_view(ident): try: @@ -327,6 +322,17 @@ def editor_changelog(ident): return render_template('editor_changelog.html', editor=editor, changelog_entries=changelog_entries) +@app.route('/editor//wip', methods=['GET']) +def editor_wip(ident): + raise NotImplementedError + try: + editor = api.get_editor(ident) + entries = api.get_editor_wip(ident) + except ApiException as ae: + abort(ae.status) + return render_template('editor_changelog.html', editor=editor, + entries=entries) + @app.route('/changelog', methods=['GET']) def changelog_view(): try: @@ -369,16 +375,33 @@ def search(): ### Auth #################################################################### -@app.route('/login') +@app.route('/auth/login') def login(): # show the user a list of login options - return render_template('release_search.html', query=query, fulltext_only=fulltext_only) + return render_template('auth_login.html') + +@app.route('/auth/token_login', methods=['GET', 'POST']) +def token_login(): + # show the user a list of login options + if 'token' in request.args: + return handle_token_login(request.args.get('token')) + if 'token' in request.form: + return handle_token_login(request.form.get('token')) + return render_template('auth_token_login.html') -@app.route('/login') +@app.route('/auth/logout') def logout(): # TODO: clear extra session info - logout_user() - return render_template('logout.html') + handle_logout() + return render_template('auth_logout.html') + +@app.route('/auth/account') +@login_required +def logout(): + # TODO: clear extra session info + handle_logout() + return render_template('auth_logout.html') + ### Static Routes ########################################################### diff --git a/python/fatcat_web/templates/auth_login.html b/python/fatcat_web/templates/auth_login.html new file mode 100644 index 00000000..98b1c7c4 --- /dev/null +++ b/python/fatcat_web/templates/auth_login.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% block body %} +

Login

+ +

via OAuth / OpenID Connect: +

+ +

Other options... +

+ +{% endblock %} diff --git a/python/fatcat_web/templates/auth_logout.html b/python/fatcat_web/templates/auth_logout.html new file mode 100644 index 00000000..819d42fe --- /dev/null +++ b/python/fatcat_web/templates/auth_logout.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} +{% block body %} +

Logout

+ +

If you are seeing this page, you are now logged out. + +

Use the links above to return to the home page or log back in. +{% endblock %} diff --git a/python/fatcat_web/templates/auth_token_login.html b/python/fatcat_web/templates/auth_token_login.html new file mode 100644 index 00000000..4c28f938 --- /dev/null +++ b/python/fatcat_web/templates/auth_token_login.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% block body %} +

Login with Token

+ +

This page is intended for operators and contingencies, not for general use. It +allows editors (users) to use an existing token (macaroon) for authentication; +a new web interface session and cookie are constructed using the token. + +
+
+
+ +{% if current_user.is_authenticated %} +

+
You are already logged in!
+

You should logout first. Re-authenticating would be undefined behavior. +

+{% else %} +
+
+
+ + +
+
+
+{% endif %} + +{% endblock %} diff --git a/python/fatcat_web/templates/base.html b/python/fatcat_web/templates/base.html index 4b3b7e0b..892ca788 100644 --- a/python/fatcat_web/templates/base.html +++ b/python/fatcat_web/templates/base.html @@ -29,17 +29,22 @@ +{% if current_user.is_authenticated %} - +{% else %} + +{% endif %} diff --git a/python/web_config.py b/python/web_config.py index 91e43e70..5713738c 100644 --- a/python/web_config.py +++ b/python/web_config.py @@ -17,14 +17,19 @@ basedir = os.path.abspath(os.path.dirname(__file__)) class Config(object): GIT_REVISION = subprocess.check_output(["git", "describe", "--always"]).strip() + # This is, effectively, the QA/PROD flag FATCAT_DOMAIN = os.environ.get("FATCAT_DOMAIN", default="qa.fatcat.wiki") + FATCAT_API_AUTH_TOKEN = os.environ.get("FATCAT_API_AUTH_TOKEN", default=None) + FATCAT_API_HOST = os.environ.get("FATCAT_API_HOST", default="https://{}/v0".format(FATCAT_DOMAIN)) + # can set this to https://search.fatcat.wiki for some experimentation ELASTICSEARCH_BACKEND = os.environ.get("ELASTICSEARCH_BACKEND", default="http://localhost:9200") ELASTICSEARCH_INDEX = os.environ.get("ELASTICSEARCH_INDEX", default="fatcat") - # bogus values for dev/testing - SECRET_KEY = os.environ.get("SECRET_KEY", default="mQLO6DpyR4t91G1tl/LPMvb/5QFV9vIUDZah5PapTUSmP8jVIrvCRw") + # for flask things, like session cookies + FLASK_SECRET_KEY = os.environ.get("FLASK_SECRET_KEY", default=None) + SECRET_KEY = FLASK_SECRET_KEY GITLAB_CLIENT_ID = os.environ.get("GITLAB_CLIENT_ID", default="bogus") GITLAB_CLIENT_SECRET = os.environ.get("GITLAB_CLIENT_SECRET", default="bogus") -- cgit v1.2.3