From 10ddca2c2fd6b14bbd94fe57aed66a6de03e1777 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Wed, 2 Jan 2019 17:58:15 -0800 Subject: start on webface oauth2/oidc web auth --- python/fatcat_web/__init__.py | 12 +++++++++++- python/fatcat_web/auth.py | 27 +++++++++++++++++++++++++++ python/fatcat_web/routes.py | 13 +++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 python/fatcat_web/auth.py (limited to 'python/fatcat_web') diff --git a/python/fatcat_web/__init__.py b/python/fatcat_web/__init__.py index 3c790e7a..f8b72fd0 100644 --- a/python/fatcat_web/__init__.py +++ b/python/fatcat_web/__init__.py @@ -2,6 +2,9 @@ from flask import Flask from flask_uuid import FlaskUUID from flask_debugtoolbar import DebugToolbarExtension +from flask_login import LoginManager +from authlib.flask.client import OAuth +from loginpass import create_flask_blueprint, Gitlab from raven.contrib.flask import Sentry from web_config import Config import fatcat_client @@ -12,6 +15,10 @@ app.config.from_object(Config) toolbar = DebugToolbarExtension(app) FlaskUUID(app) +login_manager = LoginManager() +login_manager.init_app(app) +oauth = OAuth(app) + # Grabs sentry config from SENTRY_DSN environment variable sentry = Sentry(app) @@ -19,4 +26,7 @@ conf = fatcat_client.Configuration() conf.host = "http://localhost:9411/v0" api = fatcat_client.DefaultApi(fatcat_client.ApiClient(conf)) -from fatcat_web import routes +from fatcat_web import routes, auth + +gitlab_bp = create_flask_blueprint(Gitlab, oauth, auth.handle_oauth) +app.register_blueprint(gitlab_bp, url_prefix='/auth/gitlab') diff --git a/python/fatcat_web/auth.py b/python/fatcat_web/auth.py new file mode 100644 index 00000000..f6672e87 --- /dev/null +++ b/python/fatcat_web/auth.py @@ -0,0 +1,27 @@ + +from flask import Flask, render_template, send_from_directory, request, \ + url_for, abort, g, redirect, jsonify, session +from fatcat_web import login_manager + + +# 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)) + 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 + user = UserMixin() + user.id = editor_id + return user diff --git a/python/fatcat_web/routes.py b/python/fatcat_web/routes.py index 998697bc..51533a2f 100644 --- a/python/fatcat_web/routes.py +++ b/python/fatcat_web/routes.py @@ -367,6 +367,19 @@ def search(): return render_template('release_search.html', query=query, fulltext_only=fulltext_only) +### Auth #################################################################### + +@app.route('/login') +def login(): + # show the user a list of login options + return render_template('release_search.html', query=query, fulltext_only=fulltext_only) + +@app.route('/login') +def logout(): + # TODO: clear extra session info + logout_user() + return render_template('logout.html') + ### Static Routes ########################################################### @app.errorhandler(404) -- cgit v1.2.3 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 (limited to 'python/fatcat_web') 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 From 03df0b8a6d1285fa4aa17e6c4216dd2716a9ac47 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Thu, 3 Jan 2019 21:18:10 -0800 Subject: account page and renaming --- python/fatcat_web/__init__.py | 12 +++------ python/fatcat_web/auth.py | 14 +++++++--- python/fatcat_web/routes.py | 31 ++++++++++++++++++----- python/fatcat_web/templates/auth_account.html | 27 ++++++++++++++++++++ python/fatcat_web/templates/base.html | 15 ++++++++++- python/fatcat_web/templates/editor_changelog.html | 4 +-- python/fatcat_web/templates/editor_view.html | 4 +-- 7 files changed, 85 insertions(+), 22 deletions(-) create mode 100644 python/fatcat_web/templates/auth_account.html (limited to 'python/fatcat_web') diff --git a/python/fatcat_web/__init__.py b/python/fatcat_web/__init__.py index 9cd5f812..0afee70e 100644 --- a/python/fatcat_web/__init__.py +++ b/python/fatcat_web/__init__.py @@ -23,21 +23,17 @@ oauth = OAuth(app) sentry = Sentry(app) conf = fatcat_client.Configuration() -conf.host = "http://localhost:9411/v0" +conf.host = Config.FATCAT_API_HOST api = fatcat_client.DefaultApi(fatcat_client.ApiClient(conf)) +from fatcat_web import routes, auth + 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)) + priv_api = auth.auth_api(Config.FATCAT_API_AUTH_TOKEN) 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) app.register_blueprint(gitlab_bp, url_prefix='/auth/gitlab') diff --git a/python/fatcat_web/auth.py b/python/fatcat_web/auth.py index 385f5c49..c6e6f04c 100644 --- a/python/fatcat_web/auth.py +++ b/python/fatcat_web/auth.py @@ -1,10 +1,17 @@ from flask import Flask, render_template, send_from_directory, request, \ - url_for, abort, g, redirect, jsonify, session -from fatcat_web import login_manager, api + url_for, abort, g, redirect, jsonify, session, flash +from fatcat_web import login_manager, api, Config from flask_login import logout_user, login_user, UserMixin import pymacaroons +import fatcat_client +def auth_api(token): + conf = fatcat_client.Configuration() + conf.api_key["Authorization"] = token + conf.api_key_prefix["Authorization"] = "Bearer" + conf.host = Config.FATCAT_API_HOST + return fatcat_client.DefaultApi(fatcat_client.ApiClient(conf)) def handle_logout(): logout_user() @@ -31,7 +38,7 @@ def handle_token_login(token): session['api_token'] = token session['editor'] = editor login_user(load_user(editor_id)) - return redirect("/") + return redirect("/auth/account") # This will need to login/signup via fatcatd API, then set token in session def handle_oauth(remote, token, user_info): @@ -70,6 +77,7 @@ def load_user(editor_id): token = session['api_token'] user = UserMixin() user.id = editor_id + user.editor_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 5d46fe0b..07947fd5 100644 --- a/python/fatcat_web/routes.py +++ b/python/fatcat_web/routes.py @@ -2,9 +2,10 @@ import os import json from flask import Flask, render_template, send_from_directory, request, \ - url_for, abort, g, redirect, jsonify, session + url_for, abort, g, redirect, jsonify, session, flash +from flask_login import login_required from fatcat_web import app, api -from fatcat_web.auth import handle_token_login, handle_logout +from fatcat_web.auth import handle_token_login, handle_logout, load_user, auth_api from fatcat_client.rest import ApiException from fatcat_web.search import do_search @@ -389,6 +390,23 @@ def token_login(): return handle_token_login(request.form.get('token')) return render_template('auth_token_login.html') +@app.route('/auth/change_username', methods=['POST']) +@login_required +def change_username(): + # show the user a list of login options + if not 'username' in request.form: + abort(400) + # on behalf of user... + user_api = auth_api(session['api_token']) + editor = user_api.get_editor(session['editor']['editor_id']) + editor.username = request.form['username'] + editor = user_api.update_editor(editor.editor_id, editor) + # update our session + session['editor'] = editor.to_dict() + load_user(editor.editor_id) + flash("Username updated successfully") + return redirect('/auth/account') + @app.route('/auth/logout') def logout(): # TODO: clear extra session info @@ -397,10 +415,11 @@ def logout(): @app.route('/auth/account') @login_required -def logout(): - # TODO: clear extra session info - handle_logout() - return render_template('auth_logout.html') +def auth_account(): + editor = api.get_editor(session['editor']['editor_id']) + session['editor'] = editor.to_dict() + load_user(editor.editor_id) + return render_template('auth_account.html') ### Static Routes ########################################################### diff --git a/python/fatcat_web/templates/auth_account.html b/python/fatcat_web/templates/auth_account.html new file mode 100644 index 00000000..57155722 --- /dev/null +++ b/python/fatcat_web/templates/auth_account.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% block body %} + +

Your Account

+ +

Username: {{ current_user.username }} +

Editor Id: {{ current_user.editor_id }} + +

+

Change username: +

+
+
+ + +
+
+
+
+ +

In the future, you might be able to... +

    +
  • Create a bot user +
  • Generate an API token +
+ +{% endblock %} diff --git a/python/fatcat_web/templates/base.html b/python/fatcat_web/templates/base.html index 892ca788..27b163d2 100644 --- a/python/fatcat_web/templates/base.html +++ b/python/fatcat_web/templates/base.html @@ -34,7 +34,7 @@ {{ current_user.username }}