diff --git a/plugins/printedparts/api/routes.py b/plugins/printedparts/api/routes.py index 61d5d6b..988035c 100644 --- a/plugins/printedparts/api/routes.py +++ b/plugins/printedparts/api/routes.py @@ -17,6 +17,7 @@ from shopdb.api import ( ErrorCodes, get_pagination_params, paginate_query, + require_permission, ) from ..models import PrintedItem @@ -95,6 +96,7 @@ def _mint_itemcode(item): @printedparts_bp.route('/items', methods=['POST']) @jwt_required() +@require_permission('printedparts.create') def create_item(): """Create a printed item; the itemcode is minted from the row id.""" data = request.get_json() or {} @@ -124,6 +126,7 @@ def create_item(): @printedparts_bp.route('/items/', methods=['PUT']) @jwt_required() +@require_permission('printedparts.edit') def update_item(item_id: int): """Update catalog fields. Quantity moves ONLY through the ledger.""" item = db.session.get(PrintedItem, item_id) @@ -144,6 +147,7 @@ def update_item(item_id: int): @printedparts_bp.route('/items/', methods=['DELETE']) @jwt_required() +@require_permission('printedparts.delete') def delete_item(item_id: int): """Soft-retire an item; its ledger history stays.""" item = db.session.get(PrintedItem, item_id) @@ -159,6 +163,7 @@ def delete_item(item_id: int): @printedparts_bp.route('/items//image', methods=['POST']) @jwt_required() +@require_permission('printedparts.edit') def upload_item_image(item_id: int): """Upload (or replace) the photo for an item (multipart file=).""" item = db.session.get(PrintedItem, item_id) @@ -195,6 +200,7 @@ def serve_item_image(filename): @printedparts_bp.route('/items//image', methods=['DELETE']) @jwt_required() +@require_permission('printedparts.delete') def delete_item_image(item_id: int): """Clear an item image; delete the file only if this plugin owns it.""" item = db.session.get(PrintedItem, item_id) @@ -238,6 +244,7 @@ def _ledger_write(item, transactiontype, quantitychange, sso, name, reason=None) @printedparts_bp.route('/items//restock', methods=['POST']) @jwt_required() +@require_permission('printedparts.restock') def restock_item(item_id: int): """Add freshly printed stock. Body: {quantity, badge}.""" item = db.session.get(PrintedItem, item_id) @@ -260,6 +267,7 @@ def restock_item(item_id: int): @printedparts_bp.route('/items//adjust', methods=['POST']) @jwt_required() +@require_permission('printedparts.restock') def adjust_item(item_id: int): """Correct the count (damage, recount). Body: {quantitychange, reason, badge}.""" item = db.session.get(PrintedItem, item_id) diff --git a/plugins/printedparts/plugin.py b/plugins/printedparts/plugin.py index e62b30c..c90dc3a 100644 --- a/plugins/printedparts/plugin.py +++ b/plugins/printedparts/plugin.py @@ -51,6 +51,17 @@ class PrintedpartsPlugin(BasePlugin): def init_app(self, app: Flask, db_instance) -> None: logger.info(f'Printedparts plugin initialized (v{self.meta.version})') + def get_permissions(self) -> List: + """RBAC permissions this plugin owns (seeded on install/enable).""" + return [ + ('printedparts.view', 'View 3D printed parts', 'printedparts'), + ('printedparts.create', 'Create printed parts', 'printedparts'), + ('printedparts.edit', 'Edit printed parts', 'printedparts'), + ('printedparts.delete', 'Retire printed parts', 'printedparts'), + ('printedparts.restock', 'Restock and adjust stock counts', + 'printedparts'), + ] + def get_navigation_items(self) -> List[dict]: return [ { diff --git a/tests/test_plugins/test_printedparts_ledger.py b/tests/test_plugins/test_printedparts_ledger.py index 21f4e55..81fa7db 100644 --- a/tests/test_plugins/test_printedparts_ledger.py +++ b/tests/test_plugins/test_printedparts_ledger.py @@ -119,3 +119,12 @@ def test_anonymous_cannot_mutate(client, item): json={'itemname': 'X'}).status_code == 401 assert client.post(f'/api/printedparts/items/{item}/restock', json={'quantity': 1, 'badge': '1'}).status_code == 401 + + +def test_member_without_permission_gets_403(client, member_headers, item): + """Authentication alone is not authorization: a role-less user is denied.""" + assert client.post('/api/printedparts/items', json={'itemname': 'X'}, + headers=member_headers).status_code == 403 + assert client.post(f'/api/printedparts/items/{item}/restock', + json={'quantity': 1, 'badge': '1'}, + headers=member_headers).status_code == 403