rewritten after codex recommendations
This commit is contained in:
+310
-184
@@ -1,202 +1,328 @@
|
||||
from flask import Flask, Blueprint, send_from_directory
|
||||
from Config import *
|
||||
import sys
|
||||
import datetime
|
||||
import pytz
|
||||
from FlowerServices import Flower,SuperFlower
|
||||
from AccessControl import Access
|
||||
"""HTTP routes for the Flowers browser UI and legacy form API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash, json, make_response
|
||||
from werkzeug.exceptions import HTTPException
|
||||
import secrets
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
# configuration
|
||||
# DEBUG = False
|
||||
# SECRET_KEY = 'development key'
|
||||
flower_bp = Blueprint("flower_vase", __name__, url_prefix=URLPREFIX)
|
||||
from flask import (
|
||||
Blueprint,
|
||||
abort,
|
||||
current_app,
|
||||
flash,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
|
||||
from AccessControl import Access
|
||||
from Config import DONOTSETFILTER, SESSION_MINUTES, URLPREFIX
|
||||
from FlowerServices import Flower, InvalidCredentials, StorageError, SuperFlower
|
||||
|
||||
|
||||
# app = Flask(__name__)
|
||||
# app.config.from_object(__name__)
|
||||
flower_bp = Blueprint(
|
||||
"flower_vase",
|
||||
__name__,
|
||||
url_prefix=URLPREFIX,
|
||||
static_folder="static",
|
||||
static_url_path="/static",
|
||||
template_folder="templates",
|
||||
)
|
||||
|
||||
@flower_bp.route('/static/<filename>')
|
||||
def statix(filename):
|
||||
return send_from_directory('static', filename)
|
||||
|
||||
@flower_bp.route('/')
|
||||
@dataclass
|
||||
class _Credential:
|
||||
username: str
|
||||
password: str
|
||||
expires_at: datetime
|
||||
|
||||
|
||||
class CredentialStore:
|
||||
"""Keep database credentials server-side instead of in Flask's cookie."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._items: dict[str, _Credential] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def create(self, username: str, password: str) -> str:
|
||||
token = secrets.token_urlsafe(32)
|
||||
with self._lock:
|
||||
self._purge()
|
||||
self._items[token] = _Credential(username, password, self._deadline())
|
||||
return token
|
||||
|
||||
def get(self, token: str | None) -> tuple[str, str] | None:
|
||||
if not token:
|
||||
return None
|
||||
with self._lock:
|
||||
self._purge()
|
||||
credential = self._items.get(token)
|
||||
if credential is None:
|
||||
return None
|
||||
credential.expires_at = self._deadline()
|
||||
return credential.username, credential.password
|
||||
|
||||
def revoke(self, token: str | None) -> None:
|
||||
if token:
|
||||
with self._lock:
|
||||
self._items.pop(token, None)
|
||||
|
||||
def _purge(self) -> None:
|
||||
now = datetime.now(timezone.utc)
|
||||
expired = [key for key, value in self._items.items() if value.expires_at <= now]
|
||||
for key in expired:
|
||||
del self._items[key]
|
||||
|
||||
@staticmethod
|
||||
def _deadline() -> datetime:
|
||||
return datetime.now(timezone.utc) + timedelta(minutes=SESSION_MINUTES)
|
||||
|
||||
|
||||
def _credentials() -> CredentialStore:
|
||||
return current_app.extensions["credential_store"]
|
||||
|
||||
|
||||
def _csrf_token() -> str:
|
||||
token = session.get("csrf_token")
|
||||
if not token:
|
||||
token = secrets.token_urlsafe(32)
|
||||
session["csrf_token"] = token
|
||||
return token
|
||||
|
||||
|
||||
@flower_bp.context_processor
|
||||
def template_context() -> dict:
|
||||
return {"csrf_token": _csrf_token()}
|
||||
|
||||
|
||||
@flower_bp.before_request
|
||||
def protect_browser_posts():
|
||||
if request.method != "POST" or request.endpoint == "flower_vase.web_service":
|
||||
return None
|
||||
supplied = request.form.get("csrf_token", "")
|
||||
expected = session.get("csrf_token", "")
|
||||
if not expected or not secrets.compare_digest(supplied, expected):
|
||||
abort(400, description="Invalid or expired form token")
|
||||
return None
|
||||
|
||||
|
||||
def _vault_from_session() -> Flower | None:
|
||||
credential = _credentials().get(session.get("auth_token"))
|
||||
if not credential:
|
||||
return None
|
||||
try:
|
||||
return Flower(*credential)
|
||||
except InvalidCredentials:
|
||||
return None
|
||||
|
||||
|
||||
def _login_page(name="Welcome back", password="Enter your vault password", status=200):
|
||||
return render_template(
|
||||
"index.html", name_suggestion=name, pwd_suggestion=password
|
||||
), status
|
||||
|
||||
|
||||
@flower_bp.get("/")
|
||||
def index():
|
||||
print(request.remote_addr)
|
||||
return render_template('index.html', name_suggestion="Hello", pwd_suggestion="Want to try your luck?")
|
||||
return _login_page()
|
||||
|
||||
@flower_bp.route('/browser' , methods=['POST', 'GET'])
|
||||
|
||||
@flower_bp.route("/browser", methods=["GET", "POST"])
|
||||
def application():
|
||||
now = datetime.datetime.now(pytz.timezone('Europe/Amsterdam'))
|
||||
if request.method == "GET":
|
||||
vault = _vault_from_session()
|
||||
if vault is None:
|
||||
return redirect(url_for("flower_vase.index"))
|
||||
try:
|
||||
selected_filter = session.get("filter", "")
|
||||
return render_template(
|
||||
"list.html", flowers=vault.all(), filter=selected_filter
|
||||
)
|
||||
except StorageError:
|
||||
flash("The vault database could not complete that request.", "error")
|
||||
return redirect(url_for("flower_vase.index"))
|
||||
|
||||
if request.method == 'GET':
|
||||
return render_template('index.html', name_suggestion="Faded out", pwd_suggestion="Want to retry your luck?")
|
||||
access = Access()
|
||||
client_ip = request.remote_addr or "unknown"
|
||||
if not access.granted(client_ip):
|
||||
return _login_page("Too many attempts", "Try again in five minutes", 429)
|
||||
|
||||
access_ctrl = Access()
|
||||
if not access_ctrl.granted(request.remote_addr):
|
||||
return render_template('index.html', name_suggestion="Sorry", pwd_suggestion="Access denied")
|
||||
action = request.form.get("action", "")
|
||||
if action == "login":
|
||||
return _login(access, client_ip)
|
||||
if action == "logout":
|
||||
_credentials().revoke(session.pop("auth_token", None))
|
||||
session.pop("filter", None)
|
||||
flash("You have been signed out.", "success")
|
||||
return redirect(url_for("flower_vase.index"))
|
||||
|
||||
action=request.form['action']
|
||||
filter=''
|
||||
if 'filter' in session:
|
||||
filter = session['filter']
|
||||
vault = _vault_from_session()
|
||||
if vault is None:
|
||||
session.pop("auth_token", None)
|
||||
flash("Your session expired. Please sign in again.", "error")
|
||||
return redirect(url_for("flower_vase.index"))
|
||||
|
||||
if action == 'logout':
|
||||
session['filter'] = request.form['filter']
|
||||
session['timeout'] = now - datetime.timedelta(minutes=999)
|
||||
return render_template('index.html', name_suggestion="Bye", pwd_suggestion="Come back any time")
|
||||
|
||||
|
||||
if action == 'login':
|
||||
user=request.form['name']
|
||||
pwd=request.form['password']
|
||||
F = Flower(user, pwd)
|
||||
if F.numberOfEntries()>=0:
|
||||
session['dbuser'] = user
|
||||
session['dbpwd'] = pwd
|
||||
session['timeout'] = now
|
||||
session['filter'] = filter
|
||||
return render_template('list.html', flowers=F.all(), filter=filter)
|
||||
#login failure
|
||||
access_ctrl.deny(request.remote_addr)
|
||||
print("Flowers - authorization failed for {}".format(user))
|
||||
return render_template('index.html', name_suggestion="Sorry", pwd_suggestion="Better next time")
|
||||
|
||||
if 'timeout' in session and session['timeout']+datetime.timedelta(minutes=10)>now:
|
||||
session['timeout'] = now
|
||||
|
||||
if 'filter' in request.form.keys() and len(request.form['filter'])>1 and request.form['filter'] != DONOTSETFILTER:
|
||||
session['filter']=request.form['filter']
|
||||
filter = request.form['filter']
|
||||
|
||||
F = Flower(session['dbuser'], session['dbpwd'])
|
||||
if F.numberOfEntries()>=0:
|
||||
|
||||
if action == 'list':
|
||||
return render_template('list.html', flowers=F.all(), filter=filter)
|
||||
|
||||
if action == 'show':
|
||||
return render_template('show.html', flower=F.one(request.form['organization'], request.form['datetime']))
|
||||
|
||||
if action == 'edit':
|
||||
return render_template('edit.html', flower=F.one(request.form['organization'], request.form['datetime']))
|
||||
|
||||
if action == 'new':
|
||||
return render_template('edit.html', flower=F.empty())
|
||||
|
||||
if action == 'save':
|
||||
flower = F.add(request.form['organization'],request.form['myname'],request.form['myid'],request.form['secret'])
|
||||
session['filter']=request.form['organization']
|
||||
return render_template('show.html', flower=flower)
|
||||
|
||||
if action == 'deactivate':
|
||||
flower = F.deactivate(request.form['organization'],request.form['datetime'])
|
||||
session['filter']=''
|
||||
return render_template('show.html', flower=flower)
|
||||
|
||||
if action == 'rehush':
|
||||
return render_template('rehush.html', flower=F.empty())
|
||||
|
||||
|
||||
else:
|
||||
return render_template('new.html', flower=F.empty())
|
||||
|
||||
|
||||
return render_template('index.html', name_suggestion="Sorry", pwd_suggestion="Better next time")
|
||||
|
||||
@flower_bp.route('/create' , methods=['POST','GET'])
|
||||
def create():
|
||||
access_ctrl = Access()
|
||||
if not access_ctrl.granted(request.remote_addr):
|
||||
return render_template('index.html', name_suggestion="Sorry", pwd_suggestion="Access denied")
|
||||
|
||||
if 'name' not in request.form:
|
||||
return render_template('create.html')
|
||||
newuser = request.form['name']
|
||||
newhush = request.form['password']
|
||||
|
||||
# login db with generic user
|
||||
f = SuperFlower(newuser, newhush)
|
||||
if f.createNewTable():
|
||||
# login as new user
|
||||
u = Flower(newuser, newhush)
|
||||
# add one line
|
||||
u.add('Demo organisation', 'Demo name', 'Demo login', 'Demo hush')
|
||||
|
||||
# return to the login page
|
||||
return render_template('index.html', name_suggestion="Now login", pwd_suggestion="for your very first time")
|
||||
|
||||
return render_template('create.html', message="Sorry - System error - check log files")
|
||||
|
||||
|
||||
@flower_bp.route('/update_pwd', methods=['POST','GET'])
|
||||
def update_pwd():
|
||||
if 'name' not in request.form:
|
||||
return render_template('rehush.html')
|
||||
user = request.form['name']
|
||||
oldhush = request.form['old_password']
|
||||
newhush = request.form['new_password']
|
||||
|
||||
# login db with generic user
|
||||
f = Flower(user, oldhush)
|
||||
message = f.update_pwd(newhush)
|
||||
|
||||
f = SuperFlower(user, oldhush)
|
||||
f.flush_privs()
|
||||
|
||||
return render_template('index.html', name_suggestion=message, pwd_suggestion="...")
|
||||
|
||||
|
||||
# @app.route('/migrate', methods=['POST','GET'])
|
||||
# def migrate():
|
||||
# user = 'ignace'
|
||||
# hush = 'black'
|
||||
#
|
||||
# f = SuperFlower(user, hush)
|
||||
# f.migrate(user)
|
||||
#
|
||||
# return render_template('index.html', name_suggestion='login again', pwd_suggestion="...")
|
||||
|
||||
@flower_bp.route('/app' , methods=['POST'])
|
||||
def web_service():
|
||||
# same thing as 'aplication, but returns go in json
|
||||
result = json.dumps({'result': -1, 'message': 'Invalid entry'})
|
||||
selected_filter = session.get("filter", "")
|
||||
form_filter = request.form.get("filter", "")
|
||||
if len(form_filter) > 1 and form_filter != DONOTSETFILTER:
|
||||
selected_filter = form_filter
|
||||
session["filter"] = selected_filter
|
||||
|
||||
try:
|
||||
access_ctrl = Access()
|
||||
if not access_ctrl.granted(request.remote_addr):
|
||||
result = json.dumps({'result': -1, 'message': 'Access denied'})
|
||||
if action == "list":
|
||||
return render_template("list.html", flowers=vault.all(), filter=selected_filter)
|
||||
if action in {"show", "edit"}:
|
||||
flower = vault.one(
|
||||
request.form.get("organization", ""), request.form.get("datetime", "")
|
||||
)
|
||||
return render_template(f"{action}.html", flower=flower, filter=selected_filter)
|
||||
if action == "new":
|
||||
return render_template("edit.html", flower=vault.empty(), filter=selected_filter)
|
||||
if action == "save":
|
||||
flower = vault.add(
|
||||
request.form.get("organization", "").strip(),
|
||||
request.form.get("myname", "").strip(),
|
||||
request.form.get("myid", "").strip(),
|
||||
request.form.get("secret", ""),
|
||||
)
|
||||
if flower["dateCreated"] == "FAILED":
|
||||
flash("Organization, login, and secret must each be at least four characters.", "error")
|
||||
return render_template("edit.html", flower=flower, filter=selected_filter), 422
|
||||
session["filter"] = flower["organization"]
|
||||
flash("Entry saved as a new version.", "success")
|
||||
return render_template("show.html", flower=flower, filter=flower["organization"])
|
||||
if action == "deactivate":
|
||||
flower = vault.deactivate(
|
||||
request.form.get("organization", ""), request.form.get("datetime", "")
|
||||
)
|
||||
session["filter"] = ""
|
||||
flash("Entry removed from the active list.", "success")
|
||||
return render_template("show.html", flower=flower, filter="")
|
||||
if action == "rehush":
|
||||
return render_template("rehush.html")
|
||||
except StorageError:
|
||||
flash("The vault database could not complete that request.", "error")
|
||||
return redirect(url_for("flower_vase.index"))
|
||||
|
||||
else:
|
||||
#no session variables here - the client will take care of fileter and timeout
|
||||
action = request.form['action']
|
||||
user = request.form['name']
|
||||
pwd = request.form['password']
|
||||
F = Flower(user, pwd)
|
||||
if F.numberOfEntries() == -1:
|
||||
#login failure
|
||||
access_ctrl.deny(request.remote_addr)
|
||||
result = json.dumps({'result': 0, 'message': 'Invalid username or password'})
|
||||
|
||||
elif action == 'login':
|
||||
result = json.dumps({'result': 1, 'message': 'Access granted'})
|
||||
|
||||
elif action == 'list':
|
||||
result = json.dumps({'result': 1, 'message': 'Ok', 'list': json.dumps(F.all())})
|
||||
|
||||
elif action == 'one':
|
||||
result = json.dumps({'result': 1, 'message': 'Ok', 'flower': json.dumps( F.one(request.form['organization'], request.form['dateCreated']) )})
|
||||
|
||||
elif action == 'save':
|
||||
result = json.dumps({'result': 1, 'message': 'Ok', 'flower': json.dumps(F.add(request.form['organization'],request.form['myname'],request.form['myid'],request.form['secret']) )})
|
||||
|
||||
elif action == 'deactivate':
|
||||
result = json.dumps({'result': 1, 'message': 'Ok', 'flower': json.dumps(F.deactivate(request.form['organization'], request.form['dateCreated']))})
|
||||
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
abort(400, description="Unknown action")
|
||||
|
||||
|
||||
def _login(access: Access, client_ip: str):
|
||||
username = request.form.get("name", "").strip()
|
||||
password = request.form.get("password", "")
|
||||
try:
|
||||
vault = Flower(username, password)
|
||||
authenticated = vault.authenticate()
|
||||
except InvalidCredentials:
|
||||
authenticated = False
|
||||
if not authenticated:
|
||||
access.deny(client_ip)
|
||||
return _login_page("Sign-in failed", "Check your username and password", 401)
|
||||
|
||||
old_token = session.get("auth_token")
|
||||
_credentials().revoke(old_token)
|
||||
session.clear()
|
||||
session["csrf_token"] = secrets.token_urlsafe(32)
|
||||
session["auth_token"] = _credentials().create(username, password)
|
||||
session["filter"] = ""
|
||||
return redirect(url_for("flower_vase.application"))
|
||||
|
||||
|
||||
@flower_bp.route("/create", methods=["GET", "POST"])
|
||||
def create():
|
||||
if request.method == "GET":
|
||||
return render_template("create.html")
|
||||
access = Access()
|
||||
client_ip = request.remote_addr or "unknown"
|
||||
if not access.granted(client_ip):
|
||||
return _login_page("Too many attempts", "Try again later", 429)
|
||||
|
||||
username = request.form.get("name", "").strip()
|
||||
password = request.form.get("password", "")
|
||||
try:
|
||||
provisioner = SuperFlower(username, password)
|
||||
created = provisioner.createNewTable()
|
||||
except InvalidCredentials as exc:
|
||||
return render_template("create.html", message=str(exc)), 422
|
||||
if created:
|
||||
Flower(username, password).add(
|
||||
"Demo organisation", "Demo name", "Demo login", "Demo secret"
|
||||
)
|
||||
flash("Your vault is ready. Sign in to continue.", "success")
|
||||
return redirect(url_for("flower_vase.index"))
|
||||
return render_template(
|
||||
"create.html", message="The vault could not be created. Check the server log."
|
||||
), 500
|
||||
|
||||
|
||||
@flower_bp.route("/update_pwd", methods=["GET", "POST"])
|
||||
def update_pwd():
|
||||
if request.method == "GET":
|
||||
return render_template("rehush.html")
|
||||
try:
|
||||
vault = Flower(
|
||||
request.form.get("name", "").strip(),
|
||||
request.form.get("old_password", ""),
|
||||
)
|
||||
if not vault.authenticate():
|
||||
raise InvalidCredentials("Current credentials are not valid")
|
||||
message = vault.update_pwd(request.form.get("new_password", ""))
|
||||
except InvalidCredentials as exc:
|
||||
return render_template("rehush.html", message=str(exc)), 422
|
||||
|
||||
_credentials().revoke(session.pop("auth_token", None))
|
||||
flash(message, "success" if message.startswith("SUCCESS") else "error")
|
||||
return redirect(url_for("flower_vase.index"))
|
||||
|
||||
|
||||
@flower_bp.post("/app")
|
||||
def web_service():
|
||||
"""Preserve the historical form API, including double-encoded payloads."""
|
||||
access = Access()
|
||||
client_ip = request.remote_addr or "unknown"
|
||||
if not access.granted(client_ip):
|
||||
return jsonify(result=-1, message="Access denied")
|
||||
|
||||
action = request.form.get("action", "")
|
||||
username = request.form.get("name", "")
|
||||
password = request.form.get("password", "")
|
||||
try:
|
||||
vault = Flower(username, password)
|
||||
if not vault.authenticate():
|
||||
access.deny(client_ip)
|
||||
return jsonify(result=0, message="Invalid username or password")
|
||||
|
||||
if action == "login":
|
||||
return jsonify(result=1, message="Access granted")
|
||||
if action == "list":
|
||||
return jsonify(result=1, message="Ok", list=json.dumps(vault.all()))
|
||||
if action == "one":
|
||||
item = vault.one(
|
||||
request.form.get("organization", ""),
|
||||
request.form.get("dateCreated", ""),
|
||||
)
|
||||
return jsonify(result=1, message="Ok", flower=json.dumps(item))
|
||||
if action == "save":
|
||||
item = vault.add(
|
||||
request.form.get("organization", ""),
|
||||
request.form.get("myname", ""),
|
||||
request.form.get("myid", ""),
|
||||
request.form.get("secret", ""),
|
||||
)
|
||||
return jsonify(result=1, message="Ok", flower=json.dumps(item))
|
||||
if action == "deactivate":
|
||||
item = vault.deactivate(
|
||||
request.form.get("organization", ""),
|
||||
request.form.get("dateCreated", ""),
|
||||
)
|
||||
return jsonify(result=1, message="Ok", flower=json.dumps(item))
|
||||
except (InvalidCredentials, StorageError, ValueError):
|
||||
current_app.logger.exception("Flowers API request failed")
|
||||
|
||||
return jsonify(result=-1, message="Invalid entry")
|
||||
|
||||
Reference in New Issue
Block a user