diff options
author | B Stack <bgstack15@gmail.com> | 2022-02-15 16:44:06 -0500 |
---|---|---|
committer | B Stack <bgstack15@gmail.com> | 2022-02-15 16:44:06 -0500 |
commit | bee4591ac60f8f3bd85c2f2b12dc75e09e2c97d7 (patch) | |
tree | 053806ad9cb454717d362704184c9d6c8cf6ac85 | |
parent | pw-protect /admin endpoint (diff) | |
download | stackbin-bee4591ac60f8f3bd85c2f2b12dc75e09e2c97d7.tar.gz stackbin-bee4591ac60f8f3bd85c2f2b12dc75e09e2c97d7.tar.bz2 stackbin-bee4591ac60f8f3bd85c2f2b12dc75e09e2c97d7.zip |
split auth into separate python file
Flask can use Blueprint to load paths from two separate source
files, so the endpoints and logic can be grouped logically.
-rw-r--r-- | README.md | 2 | ||||
-rwxr-xr-x | stackbin.py | 146 | ||||
-rw-r--r-- | stackbin_auth.py | 136 | ||||
-rw-r--r-- | templates/layout.html | 5 |
4 files changed, 148 insertions, 141 deletions
@@ -70,3 +70,5 @@ https://github.com/Tygs/0bin sounds cool but it uses a stack I'm unfamiliar with ## References 1. Using UUIDs instead of integers in sqlite in SQLAlchemy: [https://stackoverflow.com/questions/183042/how-can-i-use-uuids-in-sqlalchemy/812363#812363](https://stackoverflow.com/questions/183042/how-can-i-use-uuids-in-sqlalchemy/812363#812363) +2. https://stackoverflow.com/questions/15231359/split-python-flask-app-into-multiple-files/15231623#15231623 +3. https://stackoverflow.com/questions/18214612/how-to-access-app-config-in-a-blueprint/38262792#38262792 diff --git a/stackbin.py b/stackbin.py index 30b9066..56380e6 100755 --- a/stackbin.py +++ b/stackbin.py @@ -20,12 +20,12 @@ # Documentation: see README.md from datetime import datetime, timedelta from itsdangerous import Signer -from flask import (Flask, Response, request, url_for, redirect, g, render_template, session, abort) +from flask import Blueprint, Flask, Response, request, url_for, redirect, g, render_template, session, abort, current_app from flask_sqlalchemy import SQLAlchemy from werkzeug.middleware.proxy_fix import ProxyFix from pytimeparse.timeparse import timeparse # python3-pytimeparse import os -from functools import wraps +from stackbin_auth import auth, requires_session # uwsgidecorators load will fail when using initdb.py but is also not necessary try: from uwsgidecorators import timer # python3-uwsgidecorators @@ -68,6 +68,7 @@ def get_unsigned(string, salt="blank"): return Signer(app.secret_key, salt=salt).unsign(str(string)).decode("utf-8") app = Flask(__name__) +app.register_blueprint(auth) # url_prefix='/' try: app.config.from_pyfile(os.environ['STACKBIN_CONF']) except: @@ -78,64 +79,6 @@ if "STATIC_FOLDER" in app.config: if "TEMPLATE_FOLDER" in app.config: app.template_folder=app.config["TEMPLATE_FOLDER"] -def requires_session(function): - ''' - Requires a valid session, provided by cookie! - ''' - @wraps(function) - def decorated(*args, **kwargs): - if not session: - #return Response("requires session",401) - return redirect(url_for('login')) - else: - if 'user' not in session: - return Response("User is not in this session.",401) - s_user = session['user'] - c_user = request.cookies.get('user') - print(f"session user: {s_user}") - print(f"cookie user: {c_user}") - if session['user'] != c_user: - return Response("Wrong user for this session!.",401) - # otherwise, everything is good! - return function(s_user, [], *args,**kwargs) - # catch-all - #return Response("requires session",401) - return redirect(url_for('login')) - return decorated - -def requires_admin_credential(function): - """ - Requires the user pass the correct admin credential configured - in the conf file. - """ - @wraps(function) - def decorated(*args, **kwargs): - # formdata is in session if we are coming from login_basic() - form = session.get('formdata', None) - if form: - session.pop('formdata') - if 'username' in form: - username = form['username'] - if 'password' in form: - pw = form['password'] - else: - # then we are coming from the form with POST data - if 'username' not in request.form or 'password' not in request.form: - return _unauthorized_admin() - username = request.form['username'] - pw = request.form['password'] - if 'ADMIN_USERNAME' in app.config and \ - 'ADMIN_PASSWORD' in app.config and \ - username == app.config['ADMIN_USERNAME'] and pw == app.config['ADMIN_PASSWORD']: - return function(username, [], *args, **kwargs) - else: - return _unauthorized_admin() - - return decorated - -def _unauthorized_admin(): - return Response(f'<meta http-equiv="Refresh" content="4; url={url_for("login")}">Unauthorized! Invalid admin credential... returning to login form', 401) - def url_for_other_page(page): args = request.view_args.copy() args['page'] = page @@ -377,86 +320,11 @@ def get_proxied_path(): app.wsgi_app = ProxyFix(app.wsgi_app,x_for=pl,x_host=pl,x_port=pl,x_prefix=pl,x_proto=pl) return redirect(url_for('new_paste')) -@app.route("/logout") +# stubs, to simplify any templates that ask url_for("login") +@app.route('/login/') +def login(user="None"): True @app.route("/logout/") -def logout(): - resp = Response(f'<meta http-equiv="Refresh" content="1; url={url_for("new_paste")}">logged out') - # not documented but is found on the Internet in a few random places: - session.clear() - #resp.set_cookie('user','',expires=0) - return resp - -@app.route("/login/new") -@app.route("/login/new/") -def login_new(): - return redirect(url_for("login", new="")) -@app.route("/login/", methods=['POST','GET']) -def login(user="None"): - if request.method == "GET": - if 'user' in session and request.cookies.get('user') == session['user'] and (not 'new' in request.args): - return redirect(url_for("admin")) - auth_header = request.headers.get("Authorization") - # default, show login form - return redirect(url_for("login_form")) - elif request.method == "POST": - if request.authorization: - return redirect(url_for("login_basic"),code=307) - return redirect(url_for("login_generic")) - #return f"Authentication method not supported yet.",400 - -@app.route("/login/basic",methods=['POST','GET']) -@app.route("/login/basic/",methods=['POST','GET']) -def login_basic(): - if not request.authorization: - return Response(f"Please provide username and password.",401,{'WWW-Authenticate': 'Basic'}) - if 'username' not in request.authorization: - return Response(f"No username provided.",401) - if 'password' not in request.authorization: - return Response(f"No password provided.",401) - username = request.authorization.username - pw = request.authorization.password - form={'username':username,'password':pw} - session['formdata'] = form - return redirect(url_for("login_generic"),code=307) - -@app.route("/login/form", methods=['POST','GET']) -@app.route("/login/form/", methods=['POST','GET']) -def login_form(): - if request.method == "GET": - return render_template("login_form.html", - login_url = url_for("login_form") - ) - else: - # assume it is a POST - username="" - if 'username' in request.form: - username = request.form['username'] - password="" - if 'password' in request.form: - password = request.form['password'] - form={'username':username,'password':password} - session['formdata'] = form - return redirect(url_for("login_generic"), code=307) - -@app.route("/login/generic", methods=['POST','GET']) -@app.route("/login/generic/", methods=['POST','GET']) -@requires_admin_credential -def login_generic(user,groups=[]): - resp = Response(f'<meta http-equiv="Refresh" content="1; url={url_for("admin")}">success') - session['user_id'] = "admin" - resp = login_success(session,resp,user,groups) - return resp - -def login_success(session,resp,user,groups=[]): - resp.set_cookie('user',user) - #end_time = datetime.datetime.now(datetime.timezone.utc) + app.permanent_session_lifetime - #end_time_str = datetime.datetime.strftime(end_time,"%FT%TZ") - #resp.set_cookie('timestamp',end_time_str) - session.permanent = True - session['user']=user - #session['end_time'] = end_time_str - print(f"DEBUG: got valid user {user}") - return resp +def logout(): True # Initialize the database if it does not already exist db.create_all() diff --git a/stackbin_auth.py b/stackbin_auth.py new file mode 100644 index 0000000..5c12311 --- /dev/null +++ b/stackbin_auth.py @@ -0,0 +1,136 @@ +from functools import wraps +from flask import Blueprint, Flask, Response, request, url_for, redirect, g, render_template, session, abort, current_app +auth = Blueprint('auth', __name__) + +def requires_session(function): + ''' + Requires a valid session, provided by cookie! + ''' + @wraps(function) + def decorated(*args, **kwargs): + if not session: + #return Response("requires session",401) + return redirect(url_for('auth.login')) + else: + if 'user' not in session: + return Response("User is not in this session.",401) + s_user = session['user'] + c_user = request.cookies.get('user') + print(f"session user: {s_user}") + print(f"cookie user: {c_user}") + if session['user'] != c_user: + return Response("Wrong user for this session!.",401) + # otherwise, everything is good! + return function(s_user, [], *args,**kwargs) + # catch-all + #return Response("requires session",401) + return redirect(url_for('auth.login')) + return decorated + +def _requires_admin_credential(function): + """ + Requires the user pass the correct admin credential configured + in the conf file. + """ + @wraps(function) + def decorated(*args, **kwargs): + # formdata is in session if we are coming from login_basic() + form = session.get('formdata', None) + if form: + session.pop('formdata') + if 'username' in form: + username = form['username'] + if 'password' in form: + pw = form['password'] + else: + # then we are coming from the form with POST data + if 'username' not in request.form or 'password' not in request.form: + return _unauthorized_admin() + username = request.form['username'] + pw = request.form['password'] + if 'ADMIN_USERNAME' in current_app.config and \ + 'ADMIN_PASSWORD' in current_app.config and \ + username == current_app.config['ADMIN_USERNAME'] and pw == current_app.config['ADMIN_PASSWORD']: + return function(username, [], *args, **kwargs) + else: + return _unauthorized_admin() + return decorated + +def _unauthorized_admin(): + return Response(f'<meta http-equiv="Refresh" content="4; url={url_for("auth.login")}">Unauthorized! Invalid admin credential... returning to login form', 401) + +@auth.route("/logout") +@auth.route("/logout/") +def logout(): + resp = Response(f'<meta http-equiv="Refresh" content="1; url={url_for("new_paste")}">logged out') + # not documented but is found on the Internet in a few random places: + session.clear() + #resp.set_cookie('user','',expires=0) + return resp + +@auth.route("/login/new") +@auth.route("/login/new/") +def login_new(): + return redirect(url_for("auth.login", new="")) +@auth.route("/login/", methods=['POST','GET']) +def login(user="None"): + if request.method == "GET": + if 'user' in session and request.cookies.get('user') == session['user'] and (not 'new' in request.args): + return redirect(url_for("admin")) + auth_header = request.headers.get("Authorization") + # default, show login form + return redirect(url_for("auth.login_form")) + elif request.method == "POST": + if request.authorization: + return redirect(url_for("auth.login_basic"),code=307) + return redirect(url_for("auth.login_generic")) + #return f"Authentication method not supported yet.",400 + +@auth.route("/login/basic",methods=['POST','GET']) +@auth.route("/login/basic/",methods=['POST','GET']) +def login_basic(): + if not request.authorization: + return Response(f"Please provide username and password.",401,{'WWW-Authenticate': 'Basic'}) + if 'username' not in request.authorization: + return Response(f"No username provided.",401) + if 'password' not in request.authorization: + return Response(f"No password provided.",401) + username = request.authorization.username + pw = request.authorization.password + form={'username':username,'password':pw} + session['formdata'] = form + return redirect(url_for("auth.login_generic"),code=307) + +@auth.route("/login/form", methods=['POST','GET']) +@auth.route("/login/form/", methods=['POST','GET']) +def login_form(): + if request.method == "GET": + return render_template("login_form.html", + login_url = url_for("auth.login_form") + ) + else: + # assume it is a POST + username="" + if 'username' in request.form: + username = request.form['username'] + password="" + if 'password' in request.form: + password = request.form['password'] + form={'username':username,'password':password} + session['formdata'] = form + return redirect(url_for("auth.login_generic"), code=307) + +@auth.route("/login/generic", methods=['POST','GET']) +@auth.route("/login/generic/", methods=['POST','GET']) +@_requires_admin_credential +def login_generic(user,groups=[]): + resp = Response(f'<meta http-equiv="Refresh" content="1; url={url_for("admin")}">success') + session['user_id'] = "admin" + resp = login_success(session,resp,user,groups) + return resp + +def login_success(session,resp,user,groups=[]): + resp.set_cookie('user',user) + session.permanent = True + session['user']=user + return resp diff --git a/templates/layout.html b/templates/layout.html index 3620a3a..329a485 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -10,13 +10,14 @@ <ul class=nav> <li><a href="{{ url_for('new_paste') }}">New Paste</a> {% if g.user and g.user == "admin" %}<li><a href="{{ url_for('admin') }}">admin</a></li>{% endif %} - {% if g.user %}<li><a href="{{ url_for('logout') }}">Sign out</a></li>{% endif %} + {% if g.user %}<li><a href="{{ url_for('logout') }}">Sign out</a></li> + {% else %}<li><a href="{{ url_for('login') }}">Sign in</a>{% endif %} {# {% if g.user %} <li><a href="{{ url_for('my_pastes') }}">My Pastes</a> <li><a href="{{ url_for('logout') }}">Sign out ({{ g.user.display_name }})</a> {% else %} - <li><a href="{{ url_for('login') }}">Sign in with Facebook</a> + <li><a href="{{ url_for('login') }}">Sign in</a> {% endif %} #} </ul> |