111 lines
3.5 KiB
Python
111 lines
3.5 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.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
|