aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md6
-rwxr-xr-xstackbin.py148
-rw-r--r--stackbin.wsgi.ini.example7
-rw-r--r--templates/layout.html2
-rw-r--r--templates/login_form.html16
5 files changed, 170 insertions, 9 deletions
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'<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
@@ -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'<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
+
# 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 @@
<div class=page>
<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('my_pastes') }}">My Pastes</a>
diff --git a/templates/login_form.html b/templates/login_form.html
new file mode 100644
index 0000000..b179732
--- /dev/null
+++ b/templates/login_form.html
@@ -0,0 +1,16 @@
+<html>
+<head>
+<title>Login Form</title>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+</head>
+<body>
+<center>
+<h1>Login Form</h1>
+<form action="{{ login_url }}" method="post">
+<p>Username <input type="text" value="" name="username" required/></p>
+<p>Password <input type="password" name="password" required/></p>
+<p><input accesskey="s" type="submit" value="Submit"/></p>
+</form>
+</center>
+</body>
+</html>
bgstack15