From 0556ebe21c3143be2a3e32c4f49e127044bdd47c Mon Sep 17 00:00:00 2001 From: B Stack Date: Tue, 15 Feb 2022 15:12:32 -0500 Subject: pw-protect /admin endpoint Use a hardcoded password from the config file. --- README.md | 6 +- stackbin.py | 148 +++++++++++++++++++++++++++++++++++++++++++++- stackbin.wsgi.ini.example | 7 ++- templates/layout.html | 2 + templates/login_form.html | 16 +++++ 5 files changed, 170 insertions(+), 9 deletions(-) create mode 100644 templates/login_form.html diff --git a/README.md b/README.md index 6321c11..00d765d 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,7 @@ This means that if your app is behind `http://example.com/stackbin/` then you wo pip3 install --user flask-sqlalchemy pytimeparse ## Improvements -I still need to work on these tasks: - -### Development - -* Protect the /admin/ page +None at this time. ## Alternatives This is a very diverged fork of [su27/flask-pastebin](https://github.com/su27/flask-pastebin) which itself was a fork of the original [mitsuhiko/pastebin](https://github.com/mitsuhiko/flask-pastebin). The original had a few additional features worth reviewing. diff --git a/stackbin.py b/stackbin.py index 653cb9b..30b9066 100755 --- a/stackbin.py +++ b/stackbin.py @@ -20,11 +20,12 @@ # Documentation: see README.md from datetime import datetime, timedelta from itsdangerous import Signer -from flask import (Flask, request, url_for, redirect, g, render_template, session, abort) +from flask import (Flask, Response, request, url_for, redirect, g, render_template, session, abort) 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 # uwsgidecorators load will fail when using initdb.py but is also not necessary try: from uwsgidecorators import timer # python3-uwsgidecorators @@ -77,6 +78,64 @@ 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'Unauthorized! Invalid admin credential... returning to login form', 401) + def url_for_other_page(page): args = request.view_args.copy() args['page'] = page @@ -95,7 +154,8 @@ def refresh_string(delay,url): def check_user_status(): g.user = None if 'user_id' in session: - g.user = User.query.get(session['user_id']) + #g.user = User.query.get(session['user_id']) + g.user = session['user_id'] class Paste(db.Model): id = id_column() @@ -297,7 +357,8 @@ def get_all_pastes(): @app.route('/admin/') @app.route('/admin') -def admin(): +@requires_session +def admin(username, groups = []): all_pastes = get_all_pastes() return render_template('admin.html', pastes = all_pastes) @@ -316,6 +377,87 @@ 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") +@app.route("/logout/") +def logout(): + resp = Response(f'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'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 + # Initialize the database if it does not already exist db.create_all() if __name__ == "__main__": diff --git a/stackbin.wsgi.ini.example b/stackbin.wsgi.ini.example index bb30c20..d4cb995 100644 --- a/stackbin.wsgi.ini.example +++ b/stackbin.wsgi.ini.example @@ -9,7 +9,12 @@ callable = app touch-reload = /usr/libexec/stackbin/stackbin.py touch-reload = /etc/stackbin.conf touch-reload = /etc/stackbin.wsgi.ini -# the template files are not necessary, because flask always loads it from disk for each request +touch-reload = /usr/share/stackbin/templates/admin.html +touch-reload = /usr/share/stackbin/templates/layout.html +touch-reload = /usr/share/stackbin/templates/login_form.html +touch-reload = /usr/share/stackbin/templates/new_paste.html +touch-reload = /usr/share/stackbin/templates/_pagination.html +touch-reload = /usr/share/stackbin/templates/show_paste.html req-logger = file:/var/log/stackbin/req.log # to get strftime format fields, you need double percent signs logdate = "%%FT%%T" diff --git a/templates/layout.html b/templates/layout.html index 3c6d8ab..3620a3a 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -9,6 +9,8 @@