Flask + Vue rewrite of the classic-ASP shopdb, built as a plugin framework so sister GE Aerospace sites can adopt it. Product version 0.7.0-pre-0.8.0, plugin contract 0.11.0, 966 backend tests. Key entry points: - docs/INSTALL-WINDOWS-IIS.md - tested Windows + IIS install runbook (own-site and subpath deployment methods) - docs/PILOT-DEPLOY.md - production pilot: stand up + import + verify - docs/IMPORT-API.md - schema-agnostic legacy data import contract - scripts/site_imports/wjf/ - West Jefferson reference import loader - docs/adr/ - twelve accepted architecture decision records
107 lines
4.1 KiB
Python
107 lines
4.1 KiB
Python
"""Authorization decorators for role and permission gating.
|
|
|
|
Authentication (is the caller logged in?) is handled by Flask-JWT-Extended's
|
|
@jwt_required. Authorization (is the caller ALLOWED to do this?) is handled
|
|
here. The two are separate concerns; a route needs both on any state-changing
|
|
action.
|
|
|
|
These decorators call verify_jwt_in_request() themselves, so they work whether
|
|
or not a separate @jwt_required is also present. The admin role bypasses every
|
|
permission check (see User.haspermission), so an admin never needs individual
|
|
permissions granted.
|
|
|
|
Usage:
|
|
|
|
@assets_bp.route('/<int:asset_id>', methods=['DELETE'])
|
|
@jwt_required()
|
|
@require_permission('assets.delete')
|
|
def delete_asset(asset_id):
|
|
...
|
|
"""
|
|
|
|
from functools import wraps
|
|
|
|
from flask_jwt_extended import verify_jwt_in_request, current_user, get_jwt
|
|
|
|
from shopdb.utils.responses import error_response, ErrorCodes
|
|
|
|
|
|
def require_permission(permission_name: str):
|
|
"""Gate a route behind a single permission. Admin role bypasses.
|
|
|
|
A scoped personal API token (patscopes claim present) does NOT bypass: it
|
|
grants only the permissions in its scope list, intersected with what the
|
|
owner actually holds. See shopdb/utils/apitoken_auth.py.
|
|
"""
|
|
def decorator(view_func):
|
|
@wraps(view_func)
|
|
def wrapper(*args, **kwargs):
|
|
# verify jwt first so current_user is loaded (idempotent if the
|
|
# route also has @jwt_required)
|
|
verify_jwt_in_request()
|
|
if current_user is None:
|
|
return error_response(
|
|
ErrorCodes.UNAUTHORIZED,
|
|
'Authentication required',
|
|
http_code=401
|
|
)
|
|
patscopes = get_jwt().get('patscopes')
|
|
if patscopes is not None:
|
|
# Scoped PAT: allowed only when this permission is in the scope
|
|
# list AND the owner actually holds it. haspermission still
|
|
# returns True for an admin owner (who legitimately holds
|
|
# everything), so the scope list is the real limiter - the
|
|
# admin bypass is suspended.
|
|
allowed = (permission_name in patscopes
|
|
and current_user.haspermission(permission_name))
|
|
if not allowed:
|
|
return error_response(
|
|
ErrorCodes.FORBIDDEN,
|
|
'This API token is not scoped for this action',
|
|
http_code=403
|
|
)
|
|
return view_func(*args, **kwargs)
|
|
if not current_user.haspermission(permission_name):
|
|
return error_response(
|
|
ErrorCodes.FORBIDDEN,
|
|
'You do not have permission to perform this action',
|
|
http_code=403
|
|
)
|
|
return view_func(*args, **kwargs)
|
|
return wrapper
|
|
return decorator
|
|
|
|
|
|
def require_role(rolename: str):
|
|
"""Gate a route behind a single role (e.g. 'admin').
|
|
|
|
A scoped personal API token (patscopes claim present) is ALWAYS denied here:
|
|
scopes gate individual permissions, not roles, so role-gated admin surfaces
|
|
require an unscoped token. See shopdb/utils/apitoken_auth.py.
|
|
"""
|
|
def decorator(view_func):
|
|
@wraps(view_func)
|
|
def wrapper(*args, **kwargs):
|
|
verify_jwt_in_request()
|
|
if current_user is None:
|
|
return error_response(
|
|
ErrorCodes.UNAUTHORIZED,
|
|
'Authentication required',
|
|
http_code=401
|
|
)
|
|
if get_jwt().get('patscopes') is not None:
|
|
return error_response(
|
|
ErrorCodes.FORBIDDEN,
|
|
'Scoped API tokens cannot access role-gated endpoints',
|
|
http_code=403
|
|
)
|
|
if not current_user.hasrole(rolename):
|
|
return error_response(
|
|
ErrorCodes.FORBIDDEN,
|
|
f'{rolename.capitalize()} access required',
|
|
http_code=403
|
|
)
|
|
return view_func(*args, **kwargs)
|
|
return wrapper
|
|
return decorator
|