Files

112 lines
3.6 KiB
Python

"""
Central access control for set-scoped routes.
Every route that touches a set resolves it from a URL parameter — set_id,
instruction_id or file_id. Rather than bolting a permission check onto each
one (and forgetting the next one somebody adds), this installs a single
before_request hook that resolves the set behind the current request and
refuses it if the user isn't allowed.
Fail-closed by design: a set-scoped endpoint that isn't classified is
treated as needing manage rights, so a new route is restrictive until it is
deliberately listed here.
Created by: Jess Rogerson (yelling commands at Claude.AI)
"""
from flask import abort, request, flash, redirect, url_for
from flask_login import current_user
# URL parameters that identify a set, directly or via a child record.
SET_PARAM = 'set_id'
INSTRUCTION_PARAM = 'instruction_id'
EXTRA_FILE_PARAM = 'file_id'
# GET endpoints that still require manage rights — they render an editing
# surface, so showing them to a viewer would only lead to a refusal later.
MANAGE_ON_GET = {
'sets.edit_set',
'sets.debug_set_image',
'instructions.upload',
'instructions.organise',
'instructions.debug_paths',
'extra_files.upload',
}
# Endpoints that only need view rights despite being POSTs.
VIEW_ON_POST = set()
# Blueprints whose set-scoped routes this guard applies to.
GUARDED_BLUEPRINTS = {'sets', 'instructions', 'extra_files', 'moc'}
def _resolve_set():
"""Return the Set behind this request, or None if it isn't set-scoped."""
from app.models.set import Set
from app.models.instruction import Instruction
from app.models.extra_file import ExtraFile
args = request.view_args or {}
if SET_PARAM in args:
return Set.query.get(args[SET_PARAM])
if INSTRUCTION_PARAM in args:
instruction = Instruction.query.get(args[INSTRUCTION_PARAM])
return instruction.set if instruction else None
if EXTRA_FILE_PARAM in args:
extra = ExtraFile.query.get(args[EXTRA_FILE_PARAM])
return extra.lego_set if extra else None
return None
def _needs_manage(endpoint, method):
if endpoint in MANAGE_ON_GET:
return True
if endpoint in VIEW_ON_POST:
return False
return method not in ('GET', 'HEAD', 'OPTIONS')
def install_access_guard(app):
"""Register the before_request hook on `app`."""
@app.before_request
def enforce_set_access():
endpoint = request.endpoint
if not endpoint or '.' not in endpoint:
return None
blueprint = endpoint.split('.', 1)[0]
if blueprint not in GUARDED_BLUEPRINTS:
return None
args = request.view_args or {}
if not any(k in args for k in
(SET_PARAM, INSTRUCTION_PARAM, EXTRA_FILE_PARAM)):
return None
# Not logged in: let Flask-Login's own redirect happen.
if not current_user.is_authenticated:
return None
lego_set = _resolve_set()
if lego_set is None:
return None # the view's own get_or_404 will handle it
if _needs_manage(endpoint, request.method):
if not lego_set.can_manage(current_user):
# Don't confirm the set exists to someone who can't see it.
if not lego_set.can_view(current_user):
abort(404)
flash('Only the owner of this set can make that change.', 'danger')
return redirect(url_for('sets.view_set', set_id=lego_set.id))
return None
if not lego_set.can_view(current_user):
abort(404)
return None