Merge remote-tracking branch 'origin/main' into wilm
# Conflicts: # shopdb/__init__.py
This commit is contained in:
@@ -86,7 +86,6 @@ def list_apitokens():
|
||||
|
||||
|
||||
@apitokens_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('apitokens.create')
|
||||
def create_apitoken():
|
||||
"""Create a token for the caller. Returns the full secret ONCE.
|
||||
@@ -141,7 +140,6 @@ def create_apitoken():
|
||||
|
||||
|
||||
@apitokens_bp.route('/<int:tokenid>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('apitokens.create')
|
||||
def update_apitoken(tokenid: int):
|
||||
"""Rename, rescope, or deactivate a token. Own token, or any if admin.
|
||||
@@ -183,7 +181,6 @@ def update_apitoken(tokenid: int):
|
||||
|
||||
|
||||
@apitokens_bp.route('/<int:tokenid>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('apitokens.create')
|
||||
def revoke_apitoken(tokenid: int):
|
||||
"""Revoke (deactivate) a token. Own token, or any if admin."""
|
||||
|
||||
@@ -118,7 +118,7 @@ applications_bp = Blueprint('applications', __name__)
|
||||
|
||||
|
||||
@applications_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('applications.view')
|
||||
def list_applications():
|
||||
"""List all applications."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -163,7 +163,7 @@ def list_applications():
|
||||
|
||||
|
||||
@applications_bp.route('/<int:app_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('applications.view')
|
||||
def get_application(app_id: int):
|
||||
"""Get a single application with details."""
|
||||
app = db.session.get(Application, app_id)
|
||||
@@ -180,7 +180,6 @@ def get_application(app_id: int):
|
||||
|
||||
|
||||
@applications_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('applications.create')
|
||||
def create_application():
|
||||
"""Create a new application."""
|
||||
@@ -224,7 +223,6 @@ def create_application():
|
||||
|
||||
|
||||
@applications_bp.route('/<int:app_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('applications.edit')
|
||||
def update_application(app_id: int):
|
||||
"""Update an application."""
|
||||
@@ -270,7 +268,6 @@ def update_application(app_id: int):
|
||||
|
||||
|
||||
@applications_bp.route('/<int:app_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('applications.delete')
|
||||
def delete_application(app_id: int):
|
||||
"""Delete (deactivate) an application."""
|
||||
@@ -291,7 +288,7 @@ def delete_application(app_id: int):
|
||||
# ---- Versions ----
|
||||
|
||||
@applications_bp.route('/<int:app_id>/versions', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('applications.view')
|
||||
def list_versions(app_id: int):
|
||||
"""List all versions for an application."""
|
||||
app = db.session.get(Application, app_id)
|
||||
@@ -303,7 +300,6 @@ def list_versions(app_id: int):
|
||||
|
||||
|
||||
@applications_bp.route('/<int:app_id>/versions', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('applications.create')
|
||||
def create_version(app_id: int):
|
||||
"""Create a new version for an application."""
|
||||
@@ -341,7 +337,7 @@ def create_version(app_id: int):
|
||||
# ---- Computers with this app installed ----
|
||||
|
||||
@applications_bp.route('/<int:app_id>/installed', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('applications.view')
|
||||
def list_installed_machines(app_id: int):
|
||||
"""List all computers that have this application installed."""
|
||||
app = db.session.get(Application, app_id)
|
||||
@@ -380,7 +376,7 @@ def list_installed_machines(app_id: int):
|
||||
# ---- Installed Apps (per computer) ----
|
||||
|
||||
@applications_bp.route('/machines/<int:machine_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('applications.view')
|
||||
def list_machine_applications(machine_id: int):
|
||||
"""List all applications installed on a computer."""
|
||||
models, err = _require_computer_models()
|
||||
@@ -397,7 +393,6 @@ def list_machine_applications(machine_id: int):
|
||||
|
||||
|
||||
@applications_bp.route('/machines/<int:machine_id>', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('applications.create')
|
||||
def install_application(machine_id: int):
|
||||
"""Install an application on a computer."""
|
||||
@@ -449,7 +444,6 @@ def install_application(machine_id: int):
|
||||
|
||||
|
||||
@applications_bp.route('/machines/<int:machine_id>/<int:app_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('applications.delete')
|
||||
def uninstall_application(machine_id: int, app_id: int):
|
||||
"""Uninstall an application from a computer."""
|
||||
@@ -474,7 +468,6 @@ def uninstall_application(machine_id: int, app_id: int):
|
||||
|
||||
|
||||
@applications_bp.route('/machines/<int:machine_id>/<int:app_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('applications.edit')
|
||||
def update_installed_app(machine_id: int, app_id: int):
|
||||
"""Update installed application (e.g., change version)."""
|
||||
@@ -516,7 +509,6 @@ def update_installed_app(machine_id: int, app_id: int):
|
||||
# =============================================================================
|
||||
|
||||
@applications_bp.route('/<int:app_id>/image', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('applications.edit')
|
||||
def upload_application_image(app_id: int):
|
||||
"""Upload (or replace) an application's image.
|
||||
@@ -549,13 +541,13 @@ def upload_application_image(app_id: int):
|
||||
|
||||
|
||||
@applications_bp.route('/image/<path:filename>', methods=['GET'])
|
||||
@require_permission('applications.view')
|
||||
def serve_application_image(filename):
|
||||
"""Serve an uploaded application image (public - lists and tiles read it)."""
|
||||
return send_upload(_appimage_dir(), filename)
|
||||
|
||||
|
||||
@applications_bp.route('/<int:app_id>/image', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('applications.edit')
|
||||
def delete_application_image(app_id: int):
|
||||
"""Remove an application's image and clear the field."""
|
||||
@@ -574,7 +566,6 @@ def delete_application_image(app_id: int):
|
||||
|
||||
|
||||
@applications_bp.route('/<int:app_id>/package', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('applications.edit')
|
||||
def upload_application_package(app_id: int):
|
||||
"""Upload (or replace) the installer for an application.
|
||||
@@ -627,7 +618,6 @@ def upload_application_package(app_id: int):
|
||||
|
||||
|
||||
@applications_bp.route('/package/<path:filename>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@require_permission('applications.view')
|
||||
def serve_application_package(filename):
|
||||
"""Download an uploaded installer.
|
||||
@@ -640,7 +630,6 @@ def serve_application_package(filename):
|
||||
|
||||
|
||||
@applications_bp.route('/<int:app_id>/package', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('applications.edit')
|
||||
def delete_application_package(app_id: int):
|
||||
"""Remove an uploaded installer and clear the path it set."""
|
||||
|
||||
@@ -25,7 +25,7 @@ assets_bp = Blueprint('assets', __name__)
|
||||
# =============================================================================
|
||||
|
||||
@assets_bp.route('/types', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def list_asset_types():
|
||||
"""List all asset types."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -44,7 +44,7 @@ def list_asset_types():
|
||||
|
||||
|
||||
@assets_bp.route('/types/<int:type_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_asset_type(type_id: int):
|
||||
"""Get a single asset type."""
|
||||
t = db.session.get(AssetType, type_id)
|
||||
@@ -60,7 +60,6 @@ def get_asset_type(type_id: int):
|
||||
|
||||
|
||||
@assets_bp.route('/types', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.create')
|
||||
def create_asset_type():
|
||||
"""Create a new asset type."""
|
||||
@@ -92,7 +91,6 @@ def create_asset_type():
|
||||
|
||||
|
||||
@assets_bp.route('/types/<int:type_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.edit')
|
||||
def update_asset_type(type_id: int):
|
||||
"""Update an asset type's display fields (color/icon/description). The name,
|
||||
@@ -113,7 +111,7 @@ def update_asset_type(type_id: int):
|
||||
# =============================================================================
|
||||
|
||||
@assets_bp.route('/statuses', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def list_asset_statuses():
|
||||
"""List all asset statuses."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -132,7 +130,7 @@ def list_asset_statuses():
|
||||
|
||||
|
||||
@assets_bp.route('/statuses/<int:status_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_asset_status(status_id: int):
|
||||
"""Get a single asset status."""
|
||||
s = db.session.get(AssetStatus, status_id)
|
||||
@@ -148,7 +146,6 @@ def get_asset_status(status_id: int):
|
||||
|
||||
|
||||
@assets_bp.route('/statuses', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.create')
|
||||
def create_asset_status():
|
||||
"""Create a new asset status."""
|
||||
@@ -177,7 +174,6 @@ def create_asset_status():
|
||||
|
||||
|
||||
@assets_bp.route('/statuses/<int:status_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.edit')
|
||||
def update_asset_status(status_id: int):
|
||||
"""Update an asset status."""
|
||||
@@ -206,7 +202,6 @@ def update_asset_status(status_id: int):
|
||||
|
||||
|
||||
@assets_bp.route('/statuses/<int:status_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.delete')
|
||||
def delete_asset_status(status_id: int):
|
||||
"""Delete an asset status. Refused if any asset still uses it."""
|
||||
@@ -233,7 +228,7 @@ def delete_asset_status(status_id: int):
|
||||
# =============================================================================
|
||||
|
||||
@assets_bp.route('/relationshiptypes', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def list_relationship_types():
|
||||
"""List all asset relationship types."""
|
||||
types = RelationshipType.query.order_by(RelationshipType.relationshiptype).all()
|
||||
@@ -241,7 +236,6 @@ def list_relationship_types():
|
||||
|
||||
|
||||
@assets_bp.route('/relationshiptypes', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.create')
|
||||
def create_relationship_type():
|
||||
"""Create a new asset relationship type."""
|
||||
@@ -288,7 +282,6 @@ def _rel_type_dict(t):
|
||||
|
||||
|
||||
@assets_bp.route('/relationshiptypes/<int:type_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.edit')
|
||||
def update_relationship_type(type_id: int):
|
||||
"""Update a relationship type."""
|
||||
@@ -308,7 +301,6 @@ def update_relationship_type(type_id: int):
|
||||
|
||||
|
||||
@assets_bp.route('/relationshiptypes/<int:type_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.delete')
|
||||
def delete_relationship_type(type_id: int):
|
||||
"""Delete a relationship type. Refused if any relationship still uses it."""
|
||||
@@ -329,7 +321,7 @@ def delete_relationship_type(type_id: int):
|
||||
# =============================================================================
|
||||
|
||||
@assets_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def list_assets():
|
||||
"""
|
||||
List all assets with filtering and pagination.
|
||||
@@ -415,7 +407,7 @@ def list_assets():
|
||||
|
||||
|
||||
@assets_bp.route('/<int:asset_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_asset(asset_id: int):
|
||||
"""
|
||||
Get a single asset with full details.
|
||||
@@ -437,7 +429,6 @@ def get_asset(asset_id: int):
|
||||
|
||||
|
||||
@assets_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.create')
|
||||
def create_asset():
|
||||
"""Create a new asset."""
|
||||
@@ -489,7 +480,6 @@ def create_asset():
|
||||
|
||||
|
||||
@assets_bp.route('/<int:asset_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.edit')
|
||||
def update_asset(asset_id: int):
|
||||
"""Update an asset."""
|
||||
@@ -542,7 +532,6 @@ def update_asset(asset_id: int):
|
||||
|
||||
|
||||
@assets_bp.route('/<int:asset_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.delete')
|
||||
def delete_asset(asset_id: int):
|
||||
"""Delete (soft delete) an asset."""
|
||||
@@ -562,7 +551,7 @@ def delete_asset(asset_id: int):
|
||||
|
||||
|
||||
@assets_bp.route('/lookup/<assetnumber>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def lookup_asset_by_number(assetnumber: str):
|
||||
"""
|
||||
Look up an asset by its asset number.
|
||||
@@ -683,7 +672,7 @@ def propagate_relationship(rel):
|
||||
|
||||
|
||||
@assets_bp.route('/<int:asset_id>/relationships', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_asset_relationships(asset_id: int):
|
||||
"""
|
||||
Get all relationships for an asset.
|
||||
@@ -733,7 +722,6 @@ def get_asset_relationships(asset_id: int):
|
||||
|
||||
|
||||
@assets_bp.route('/relationships', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.create')
|
||||
def create_asset_relationship():
|
||||
"""Create a relationship between two assets.
|
||||
@@ -884,7 +872,6 @@ def create_asset_relationship():
|
||||
|
||||
|
||||
@assets_bp.route('/relationships/<int:rel_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.delete')
|
||||
def delete_asset_relationship(rel_id: int):
|
||||
"""Delete an asset relationship.
|
||||
@@ -917,7 +904,7 @@ def delete_asset_relationship(rel_id: int):
|
||||
# =============================================================================
|
||||
|
||||
@assets_bp.route('/map', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_assets_map():
|
||||
"""
|
||||
Get all assets with map positions for unified floor map display.
|
||||
@@ -1239,7 +1226,7 @@ def get_assets_map():
|
||||
|
||||
|
||||
@assets_bp.route('/<int:asset_id>/communications', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_asset_communications(asset_id: int):
|
||||
"""Get all communications for an asset."""
|
||||
from shopdb.core.models import Communication
|
||||
|
||||
@@ -52,7 +52,6 @@ def _resolve_full_names(ssos):
|
||||
|
||||
|
||||
@auditlogs_bp.route('', methods=['GET'])
|
||||
@jwt_required()
|
||||
@require_permission('audit.view')
|
||||
def list_auditlogs():
|
||||
"""
|
||||
@@ -136,7 +135,6 @@ def list_auditlogs():
|
||||
|
||||
|
||||
@auditlogs_bp.route('/entity/<entitytype>/<int:entityid>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@require_permission('audit.view')
|
||||
def get_entity_history(entitytype: str, entityid: int):
|
||||
"""Get audit history for a specific entity."""
|
||||
@@ -149,7 +147,6 @@ def get_entity_history(entitytype: str, entityid: int):
|
||||
|
||||
|
||||
@auditlogs_bp.route('/stats', methods=['GET'])
|
||||
@jwt_required()
|
||||
@require_permission('audit.view')
|
||||
def get_stats():
|
||||
"""Get audit log statistics."""
|
||||
@@ -198,7 +195,6 @@ def db_func_count_by(column):
|
||||
|
||||
|
||||
@auditlogs_bp.route('/retention', methods=['GET'])
|
||||
@jwt_required()
|
||||
@require_permission('audit.view')
|
||||
def get_retention():
|
||||
"""Size of the log and what the configured retention would remove.
|
||||
@@ -212,7 +208,6 @@ def get_retention():
|
||||
|
||||
|
||||
@auditlogs_bp.route('/purge', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('audit.purge')
|
||||
def purge_auditlogs():
|
||||
"""Delete entries older than the retention window.
|
||||
|
||||
@@ -15,6 +15,7 @@ from shopdb.utils.pagination import get_pagination_params, paginate_query
|
||||
|
||||
from shopdb.utils.authz import require_role
|
||||
from shopdb.utils.import_mode import apply_import_timestamps
|
||||
from shopdb.utils.authz import require_permission
|
||||
|
||||
businessunits_bp = Blueprint('businessunits', __name__)
|
||||
|
||||
@@ -51,7 +52,7 @@ def list_businessunits():
|
||||
|
||||
|
||||
@businessunits_bp.route('/<int:bu_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_businessunit(bu_id: int):
|
||||
"""Get a single business unit."""
|
||||
bu = db.session.get(BusinessUnit, bu_id)
|
||||
|
||||
@@ -15,6 +15,7 @@ from shopdb.utils.responses import (
|
||||
ErrorCodes
|
||||
)
|
||||
from shopdb.utils.authz import require_role
|
||||
from shopdb.utils.authz import require_permission
|
||||
|
||||
customfields_bp = Blueprint('customfields', __name__)
|
||||
|
||||
@@ -45,7 +46,7 @@ def _normalize_options(raw):
|
||||
# =============================================================================
|
||||
|
||||
@customfields_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def list_fields():
|
||||
"""List custom-field definitions. Filter by ?assettypeid=. ?active=false
|
||||
includes inactive ones."""
|
||||
@@ -171,7 +172,7 @@ def _fields_with_values(asset):
|
||||
|
||||
|
||||
@customfields_bp.route('/asset/<int:assetid>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_asset_fields(assetid):
|
||||
"""Active custom fields for an asset's type, merged with its values."""
|
||||
asset = db.session.get(Asset, assetid)
|
||||
|
||||
@@ -6,6 +6,7 @@ from flask_jwt_extended import jwt_required
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models import Asset, AssetType, AssetStatus
|
||||
from shopdb.utils.responses import success_response
|
||||
from shopdb.utils.authz import require_permission
|
||||
|
||||
dashboard_bp = Blueprint('dashboard', __name__)
|
||||
|
||||
@@ -65,7 +66,7 @@ def _countedassets():
|
||||
|
||||
@dashboard_bp.route('/summary', methods=['GET'])
|
||||
@dashboard_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_dashboard():
|
||||
"""Get dashboard summary data (asset-based)."""
|
||||
machine_count = _count_by_type('machine')
|
||||
@@ -121,7 +122,7 @@ def get_dashboard():
|
||||
|
||||
|
||||
@dashboard_bp.route('/stats', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_stats():
|
||||
"""Get detailed statistics by asset type."""
|
||||
type_counts = db.session.query(
|
||||
@@ -154,9 +155,14 @@ def get_navigation():
|
||||
all_items = []
|
||||
|
||||
# Core navigation items (always present)
|
||||
# Every item names the permission its page needs, so the filter below can
|
||||
# hide what the caller cannot reach - a link that only leads to a 401 is
|
||||
# worse than no link.
|
||||
all_items.extend([
|
||||
{'name': 'Dashboard', 'icon': 'layout-dashboard', 'route': '/', 'position': 0},
|
||||
{'name': 'Map', 'icon': 'map', 'route': '/map', 'position': 4},
|
||||
{'name': 'Dashboard', 'icon': 'layout-dashboard', 'route': '/',
|
||||
'position': 0, 'permission': 'assets.view'},
|
||||
{'name': 'Map', 'icon': 'map', 'route': '/map', 'position': 4,
|
||||
'permission': 'assets.view'},
|
||||
])
|
||||
|
||||
# Collect navigation items from enabled plugins. Disabling persists to the
|
||||
@@ -180,18 +186,41 @@ def get_navigation():
|
||||
|
||||
# Add core information section items
|
||||
all_items.extend([
|
||||
{'name': 'Applications', 'icon': 'app-window', 'route': '/applications', 'position': 30, 'section': 'information'},
|
||||
{'name': 'Reports', 'icon': 'bar-chart-3', 'route': '/reports', 'position': 40, 'section': 'information'},
|
||||
{'name': 'Applications', 'icon': 'app-window', 'route': '/applications',
|
||||
'position': 30, 'section': 'information',
|
||||
'permission': 'applications.view'},
|
||||
{'name': 'Reports', 'icon': 'bar-chart-3', 'route': '/reports',
|
||||
'position': 40, 'section': 'information', 'permission': 'reports.view'},
|
||||
])
|
||||
|
||||
# Sort by position
|
||||
all_items.sort(key=lambda x: x.get('position', 99))
|
||||
|
||||
# A signed-out visitor is shown only what the guest role can actually
|
||||
# reach. Without this the sidebar offers them the whole app and every link
|
||||
# bounces off a 401, which reads as a broken site rather than a closed
|
||||
# door. An item with no declared permission is treated as members-only: a
|
||||
# plugin opts into being guest-visible by naming the permission its page
|
||||
# needs, which is the same string the API already gates on.
|
||||
from flask_jwt_extended import verify_jwt_in_request
|
||||
from shopdb.utils.authz import currentuser, guestholds
|
||||
|
||||
verify_jwt_in_request(optional=True)
|
||||
def guestmayreach(item):
|
||||
declared = item.get('permission')
|
||||
if not declared:
|
||||
return False
|
||||
names = declared if isinstance(declared, list) else [declared]
|
||||
return any(guestholds(name) for name in names)
|
||||
|
||||
if currentuser() is None:
|
||||
all_items = [item for item in all_items if guestmayreach(item)]
|
||||
|
||||
return success_response(all_items)
|
||||
|
||||
|
||||
@dashboard_bp.route('/widgets', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_widgets():
|
||||
"""Aggregate dashboard widget definitions from all enabled plugins.
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from shopdb.core.models.dashboarddefault import (
|
||||
DISPLAY_ROLES, DISPLAY_ROLE_PATHS, normalize_display_role)
|
||||
from shopdb.utils.responses import success_response, error_response, ErrorCodes
|
||||
from shopdb.utils.authz import require_role
|
||||
from shopdb.utils.authz import require_permission
|
||||
|
||||
dashboarddefaults_bp = Blueprint('dashboarddefaults', __name__)
|
||||
|
||||
@@ -102,7 +103,7 @@ def display_role():
|
||||
|
||||
|
||||
@dashboarddefaults_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('settings.view')
|
||||
def list_defaults():
|
||||
"""List all visitor-IP -> business-unit mappings."""
|
||||
defaults = DashboardDefault.query.filter_by(isactive=True).order_by(
|
||||
|
||||
@@ -15,6 +15,7 @@ from shopdb.utils.pagination import get_pagination_params, paginate_query
|
||||
|
||||
from shopdb.utils.authz import require_role
|
||||
from shopdb.utils.import_mode import apply_import_timestamps
|
||||
from shopdb.utils.authz import require_permission
|
||||
|
||||
locations_bp = Blueprint('locations', __name__)
|
||||
|
||||
@@ -30,7 +31,7 @@ def _loc_type_dict(t):
|
||||
|
||||
|
||||
@locations_bp.route('/types', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def list_location_types():
|
||||
"""List location types. ?active=false includes inactive ones."""
|
||||
query = LocationType.query
|
||||
@@ -101,7 +102,7 @@ def delete_location_type(type_id):
|
||||
|
||||
|
||||
@locations_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def list_locations():
|
||||
"""List all locations."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -132,7 +133,7 @@ def list_locations():
|
||||
|
||||
|
||||
@locations_bp.route('/<int:location_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_location(location_id: int):
|
||||
"""Get a single location."""
|
||||
loc = db.session.get(Location, location_id)
|
||||
|
||||
@@ -20,6 +20,7 @@ from shopdb.utils.responses import success_response, error_response, ErrorCodes
|
||||
from shopdb.utils.authz import require_role
|
||||
from shopdb.api import send_upload
|
||||
from shopdb.utils.imagesize import image_size
|
||||
from shopdb.utils.authz import require_permission
|
||||
|
||||
maplevels_bp = Blueprint('maplevels', __name__)
|
||||
|
||||
@@ -38,6 +39,7 @@ def _blueprint_dir():
|
||||
# =============================================================================
|
||||
|
||||
@maplevels_bp.route('', methods=['GET'])
|
||||
@require_permission('assets.view')
|
||||
def list_levels():
|
||||
"""Every active level, grouped by building, in display order.
|
||||
|
||||
@@ -72,6 +74,7 @@ def list_levels():
|
||||
|
||||
|
||||
@maplevels_bp.route('/<int:levelid>', methods=['GET'])
|
||||
@require_permission('assets.view')
|
||||
def get_level(levelid):
|
||||
level = db.session.get(MapLevel, levelid)
|
||||
if not level or not level.isactive:
|
||||
@@ -81,6 +84,7 @@ def get_level(levelid):
|
||||
|
||||
|
||||
@maplevels_bp.route('/<int:levelid>/blueprint/<path:filename>', methods=['GET'])
|
||||
@require_permission('assets.view')
|
||||
def serve_blueprint(filename, levelid=None):
|
||||
"""Serve a level's blueprint image. Public, for the same reason as above."""
|
||||
return send_upload(_blueprint_dir(), filename)
|
||||
|
||||
@@ -115,7 +115,6 @@ def apply_transform(asset, transform):
|
||||
|
||||
|
||||
@mappositions_bp.route('/transform', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.edit')
|
||||
def transform_positions():
|
||||
"""Move every placed marker on a level by a transform read off landmarks.
|
||||
@@ -225,7 +224,6 @@ def transform_positions():
|
||||
# =============================================================================
|
||||
|
||||
@mappositions_bp.route('/positions', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.edit')
|
||||
def set_positions():
|
||||
"""Set positions for many assets at once.
|
||||
@@ -301,7 +299,6 @@ def set_positions():
|
||||
|
||||
|
||||
@mappositions_bp.route('/verify', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.edit')
|
||||
def verify_positions():
|
||||
"""Mark markers as reviewed against the current drawing without moving them.
|
||||
@@ -330,7 +327,6 @@ def verify_positions():
|
||||
# =============================================================================
|
||||
|
||||
@mappositions_bp.route('/snapshots', methods=['GET'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.view')
|
||||
def list_snapshots():
|
||||
"""Snapshots, newest first. Metadata only - the positions are large."""
|
||||
@@ -341,7 +337,6 @@ def list_snapshots():
|
||||
|
||||
|
||||
@mappositions_bp.route('/snapshots/<int:snapshotid>/restore', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('assets.edit')
|
||||
def restore_snapshot(snapshotid):
|
||||
"""Put the positions in this snapshot back.
|
||||
|
||||
@@ -20,6 +20,7 @@ from shopdb.utils.pagination import get_pagination_params, paginate_query
|
||||
from shopdb.utils.authz import require_role
|
||||
from shopdb.utils.import_mode import apply_import_timestamps
|
||||
from shopdb.utils.uploads import send_upload
|
||||
from shopdb.utils.authz import require_permission
|
||||
|
||||
models_bp = Blueprint('models', __name__)
|
||||
|
||||
@@ -39,7 +40,7 @@ def _modelimage_dir():
|
||||
|
||||
|
||||
@models_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def list_models():
|
||||
"""List all vendor catalog models."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -85,7 +86,7 @@ def list_models():
|
||||
|
||||
|
||||
@models_bp.route('/<int:model_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_model(model_id: int):
|
||||
"""Get a single model."""
|
||||
m = db.session.get(Model, model_id)
|
||||
@@ -234,6 +235,7 @@ def upload_model_image(model_id: int):
|
||||
|
||||
|
||||
@models_bp.route('/image/<path:filename>', methods=['GET'])
|
||||
@require_permission('assets.view')
|
||||
def serve_model_image(filename):
|
||||
"""Serve an uploaded model image (public - asset detail pages read it)."""
|
||||
return send_upload(_modelimage_dir(), filename)
|
||||
|
||||
@@ -19,12 +19,13 @@ from shopdb.utils.pagination import get_pagination_params, paginate_query
|
||||
|
||||
from shopdb.utils.authz import require_role
|
||||
from shopdb.utils.import_mode import apply_import_timestamps
|
||||
from shopdb.utils.authz import require_permission
|
||||
|
||||
modeltypes_bp = Blueprint('modeltypes', __name__)
|
||||
|
||||
|
||||
@modeltypes_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def list_modeltypes():
|
||||
"""List all model types with optional filtering."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -53,7 +54,7 @@ def list_modeltypes():
|
||||
|
||||
|
||||
@modeltypes_bp.route('/<int:type_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_modeltype(type_id: int):
|
||||
"""Get a single model type."""
|
||||
mt = db.session.get(ModelType, type_id)
|
||||
|
||||
@@ -15,12 +15,13 @@ from shopdb.utils.pagination import get_pagination_params, paginate_query
|
||||
|
||||
from shopdb.utils.authz import require_role
|
||||
from shopdb.utils.import_mode import apply_import_timestamps
|
||||
from shopdb.utils.authz import require_permission
|
||||
|
||||
operatingsystems_bp = Blueprint('operatingsystems', __name__)
|
||||
|
||||
|
||||
@operatingsystems_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def list_operatingsystems():
|
||||
"""List all operating systems."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -49,7 +50,7 @@ def list_operatingsystems():
|
||||
|
||||
|
||||
@operatingsystems_bp.route('/<int:os_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_operatingsystem(os_id: int):
|
||||
"""Get a single operating system."""
|
||||
os = db.session.get(OperatingSystem, os_id)
|
||||
|
||||
@@ -6,12 +6,13 @@ from flask_jwt_extended import jwt_required
|
||||
from shopdb.utils.responses import success_response, error_response, ErrorCodes
|
||||
|
||||
from shopdb.utils.authz import require_role
|
||||
from shopdb.utils.authz import require_permission
|
||||
|
||||
plugins_bp = Blueprint('plugins', __name__)
|
||||
|
||||
|
||||
@plugins_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('settings.view')
|
||||
def list_plugins():
|
||||
"""List all discovered plugins (enabled or not) with their metadata and
|
||||
the framework contract version."""
|
||||
|
||||
@@ -15,6 +15,7 @@ from flask_jwt_extended import jwt_required
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models import Asset
|
||||
from shopdb.utils.responses import success_response, error_response, ErrorCodes
|
||||
from shopdb.utils.authz import require_permission
|
||||
|
||||
pluginui_bp = Blueprint('pluginui', __name__)
|
||||
|
||||
@@ -47,7 +48,7 @@ def _collect(hookname):
|
||||
|
||||
|
||||
@pluginui_bp.route('/settings-cards', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def settings_cards():
|
||||
"""Merge enabled plugins' settings-catalog cards (get_settings_cards hook).
|
||||
|
||||
@@ -60,7 +61,7 @@ def settings_cards():
|
||||
|
||||
|
||||
@pluginui_bp.route('/asset-panels', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def asset_panels():
|
||||
"""Return the asset-detail panels matching one asset's type (get_asset_panels).
|
||||
|
||||
@@ -86,7 +87,7 @@ def asset_panels():
|
||||
|
||||
|
||||
@pluginui_bp.route('/map-overlays', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def map_overlays():
|
||||
"""Merge enabled plugins' map overlay declarations (get_map_overlays hook).
|
||||
|
||||
@@ -98,7 +99,7 @@ def map_overlays():
|
||||
|
||||
|
||||
@pluginui_bp.route('/asset-presentation', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def asset_presentation():
|
||||
"""Merge enabled plugins' asset-type presentation entries (hook).
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ def generate_csv(data: list, columns: list) -> str:
|
||||
# =============================================================================
|
||||
|
||||
@reports_bp.route('/machines-by-type', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('reports.view')
|
||||
def machines_by_type():
|
||||
"""
|
||||
Report: Machine count grouped by machine type.
|
||||
@@ -107,7 +107,7 @@ def machines_by_type():
|
||||
# =============================================================================
|
||||
|
||||
@reports_bp.route('/assets-by-status', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('reports.view')
|
||||
def assets_by_status():
|
||||
"""
|
||||
Report: Asset count grouped by status.
|
||||
@@ -166,7 +166,7 @@ def assets_by_status():
|
||||
# =============================================================================
|
||||
|
||||
@reports_bp.route('/kb-popularity', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('reports.view')
|
||||
def kb_popularity():
|
||||
"""
|
||||
Report: Most clicked knowledge base articles.
|
||||
@@ -223,7 +223,7 @@ def kb_popularity():
|
||||
# =============================================================================
|
||||
|
||||
@reports_bp.route('/software-compliance', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('reports.view')
|
||||
def software_compliance():
|
||||
"""
|
||||
Report: Required applications vs installed (per PC).
|
||||
@@ -342,7 +342,7 @@ def software_compliance():
|
||||
# =============================================================================
|
||||
|
||||
@reports_bp.route('/asset-inventory', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('reports.view')
|
||||
def asset_inventory():
|
||||
"""
|
||||
Report: Complete asset inventory summary.
|
||||
@@ -456,7 +456,7 @@ def asset_inventory():
|
||||
# =============================================================================
|
||||
|
||||
@reports_bp.route('/pc-relationships', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('reports.view')
|
||||
def pc_relationships():
|
||||
"""
|
||||
Report: PCs with relationships to shop floor machines.
|
||||
@@ -525,7 +525,6 @@ def pc_relationships():
|
||||
|
||||
|
||||
@reports_bp.route('/email', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('reports.export')
|
||||
def email_report():
|
||||
"""Email a report's data as an HTML table (on-demand delivery).
|
||||
@@ -574,7 +573,7 @@ def email_report():
|
||||
|
||||
|
||||
@reports_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('reports.view')
|
||||
def list_reports():
|
||||
"""List all available reports."""
|
||||
reports = [
|
||||
|
||||
@@ -18,6 +18,7 @@ from shopdb.core.models import (
|
||||
from shopdb.core.api.settings import get_cached_settings
|
||||
from shopdb.core.services.spellfix import Vocabulary
|
||||
from shopdb.utils.responses import success_response
|
||||
from shopdb.utils.authz import require_permission
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1161,7 +1162,7 @@ def _suggestion_for(query):
|
||||
|
||||
|
||||
@search_bp.route('/suggest', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def search_suggest():
|
||||
"""A better spelling for a query that found nothing, or null.
|
||||
|
||||
@@ -1185,7 +1186,7 @@ def search_suggest():
|
||||
|
||||
|
||||
@search_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def global_search():
|
||||
"""
|
||||
Global search across multiple entity types.
|
||||
|
||||
@@ -576,7 +576,6 @@ def get_setting(key: str):
|
||||
|
||||
|
||||
@settings_bp.route('/<key>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@require_permission('settings.edit')
|
||||
def update_setting(key: str):
|
||||
"""Update a setting value."""
|
||||
@@ -635,7 +634,6 @@ def update_setting(key: str):
|
||||
|
||||
|
||||
@settings_bp.route('/test-email', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('settings.edit')
|
||||
def test_email():
|
||||
"""Send a test email to verify SMTP configuration.
|
||||
@@ -673,7 +671,6 @@ def test_email():
|
||||
|
||||
|
||||
@settings_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('settings.edit')
|
||||
def create_setting():
|
||||
"""Create a new setting (admin only)."""
|
||||
@@ -1299,7 +1296,6 @@ def build_default_settings():
|
||||
|
||||
|
||||
@settings_bp.route('/seed', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('settings.edit')
|
||||
def seed_default_settings():
|
||||
"""Seed default settings if they don't exist."""
|
||||
|
||||
@@ -14,12 +14,13 @@ from shopdb.utils.responses import (
|
||||
)
|
||||
from shopdb.utils.authz import require_role
|
||||
from shopdb.utils.import_mode import apply_import_timestamps
|
||||
from shopdb.utils.authz import require_permission
|
||||
|
||||
supportteams_bp = Blueprint('supportteams', __name__)
|
||||
|
||||
|
||||
@supportteams_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def list_support_teams():
|
||||
"""List support teams (with contacts). ?active and ?teamname filters."""
|
||||
query = SupportTeam.query
|
||||
@@ -36,7 +37,7 @@ def list_support_teams():
|
||||
|
||||
|
||||
@supportteams_bp.route('/<int:team_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_support_team(team_id: int):
|
||||
"""Get a single support team with its contacts."""
|
||||
team = db.session.get(SupportTeam, team_id)
|
||||
|
||||
@@ -6,7 +6,8 @@ from werkzeug.security import generate_password_hash
|
||||
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models import (
|
||||
User, Role, Permission, AuditLog, full_permission_catalog)
|
||||
User, Role, Permission, AuditLog, full_permission_catalog,
|
||||
guest_grantable_permissions)
|
||||
from shopdb.utils.responses import success_response, error_response, ErrorCodes
|
||||
from shopdb.utils.authz import require_role
|
||||
|
||||
@@ -266,6 +267,9 @@ def list_permissions():
|
||||
def list_roles():
|
||||
"""List all roles with their permissions."""
|
||||
roles = Role.query.order_by(Role.rolename).all()
|
||||
# What a guest MAY be granted, so the grid can grey out the rest rather
|
||||
# than letting somebody tick a box the API will refuse.
|
||||
guestgrantable = sorted(guest_grantable_permissions())
|
||||
return success_response([{
|
||||
'roleid': r.roleid,
|
||||
'rolename': r.rolename,
|
||||
@@ -273,7 +277,9 @@ def list_roles():
|
||||
'color': r.color,
|
||||
'usercount': r.users.count(),
|
||||
'permissions': [p.name for p in r.permissions],
|
||||
'isadmin': r.rolename == 'admin'
|
||||
'isadmin': r.rolename == 'admin',
|
||||
'isguest': r.rolename == Role.GUEST,
|
||||
'grantable': guestgrantable if r.rolename == Role.GUEST else None,
|
||||
} for r in roles])
|
||||
|
||||
|
||||
@@ -344,6 +350,20 @@ def update_role(roleid: int):
|
||||
|
||||
# Update permissions
|
||||
if 'permissions' in data and role.rolename != 'admin':
|
||||
# The guest role is anyone with no account, so what it may hold is
|
||||
# bounded: reads, plus the handful of writes a plugin has explicitly
|
||||
# declared safe to do anonymously. Refused at the door rather than
|
||||
# warned about - a checkbox that quietly makes deleting an asset
|
||||
# anonymous is not a warning anybody reads twice.
|
||||
if role.rolename == Role.GUEST:
|
||||
allowed = guest_grantable_permissions()
|
||||
refused = sorted(set(data['permissions']) - allowed)
|
||||
if refused:
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
'The guest role cannot hold {}. A guest may be granted any '
|
||||
'view permission, and only writes a plugin has declared '
|
||||
'safe to do without an account.'.format(', '.join(refused)))
|
||||
old_perms = [p.name for p in role.permissions]
|
||||
perms = Permission.query.filter(Permission.name.in_(data['permissions'])).all()
|
||||
new_perms = [p.name for p in perms]
|
||||
@@ -377,6 +397,14 @@ def delete_role(roleid: int):
|
||||
if role.rolename == 'admin':
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'Cannot delete the admin role')
|
||||
|
||||
# Deleting the guest role does not close the door, it removes the only
|
||||
# place the door is described. Revoke its permissions instead.
|
||||
if role.rolename == Role.GUEST:
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
'Cannot delete the guest role. To close guest access, clear its '
|
||||
'permissions.')
|
||||
|
||||
if role.users.count() > 0:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, f'Role is assigned to {role.users.count()} user(s)')
|
||||
|
||||
|
||||
@@ -15,12 +15,13 @@ from shopdb.utils.pagination import get_pagination_params, paginate_query
|
||||
|
||||
from shopdb.utils.authz import require_role
|
||||
from shopdb.utils.import_mode import apply_import_timestamps
|
||||
from shopdb.utils.authz import require_permission
|
||||
|
||||
vendors_bp = Blueprint('vendors', __name__)
|
||||
|
||||
|
||||
@vendors_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def list_vendors():
|
||||
"""List all vendors."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -46,7 +47,7 @@ def list_vendors():
|
||||
|
||||
|
||||
@vendors_bp.route('/<int:vendor_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('assets.view')
|
||||
def get_vendor(vendor_id: int):
|
||||
"""Get a single vendor."""
|
||||
v = db.session.get(Vendor, vendor_id)
|
||||
|
||||
Reference in New Issue
Block a user