read permissions on every GET

This commit is contained in:
2026-08-27 14:26:17 -04:00
parent 4da6c6aa36
commit 0c1fbfcc1e
51 changed files with 521 additions and 224 deletions

View File

@@ -17,7 +17,7 @@ computers_bp = Blueprint('computers', __name__)
# =============================================================================
@computers_bp.route('/types', methods=['GET'])
@jwt_required(optional=True)
@require_permission('computers.view')
def list_computer_types():
"""List all computer types."""
page, per_page = get_pagination_params(request)
@@ -39,7 +39,7 @@ def list_computer_types():
@computers_bp.route('/types/<int:type_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('computers.view')
def get_computer_type(type_id: int):
"""Get a single computer type."""
t = db.session.get(ComputerType, type_id)
@@ -145,7 +145,7 @@ def delete_computer_type(type_id: int):
# =============================================================================
@computers_bp.route('/protocols', methods=['GET'])
@jwt_required(optional=True)
@require_permission('computers.view')
def list_protocols():
"""List access protocols. ?active=false includes disabled ones."""
query = AccessProtocol.query
@@ -293,7 +293,7 @@ def _sync_access_methods(comp, data):
# =============================================================================
@computers_bp.route('/display-kiosks', methods=['GET'])
@jwt_required(optional=True)
@require_permission('computers.view')
def list_display_kiosks():
"""Reporting display kiosks, for the Dashboard Defaults picker.
@@ -329,7 +329,7 @@ def list_display_kiosks():
@computers_bp.route('', methods=['GET'])
@jwt_required(optional=True)
@require_permission('computers.view')
def list_computers():
"""
List all computers with filtering and pagination.
@@ -437,7 +437,7 @@ def list_computers():
@computers_bp.route('/<int:computer_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('computers.view')
def get_computer(computer_id: int):
"""Get a single computer with full details."""
comp = db.session.get(Computer, computer_id)
@@ -461,7 +461,7 @@ def get_computer(computer_id: int):
@computers_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('computers.view')
def get_computer_by_asset(asset_id: int):
"""Get computer data by asset ID."""
comp = Computer.query.filter_by(assetid=asset_id).first()
@@ -480,7 +480,7 @@ def get_computer_by_asset(asset_id: int):
@computers_bp.route('/by-hostname/<hostname>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('computers.view')
def get_computer_by_hostname(hostname: str):
"""Get computer by hostname."""
comp = Computer.query.filter_by(hostname=hostname).first()
@@ -753,7 +753,7 @@ def delete_computer(computer_id: int):
# =============================================================================
@computers_bp.route('/<int:computer_id>/apps', methods=['GET'])
@jwt_required(optional=True)
@require_permission('computers.view')
def get_installed_apps(computer_id: int):
"""Get all installed applications for a computer."""
comp = db.session.get(Computer, computer_id)
@@ -897,7 +897,7 @@ def report_status(computer_id: int):
# =============================================================================
@computers_bp.route('/dashboard/summary', methods=['GET'])
@jwt_required(optional=True)
@require_permission('computers.view')
def dashboard_summary():
"""Get computer dashboard summary data."""
# Total active computers

View File

@@ -29,6 +29,7 @@ from shopdb.api import Setting
from shopdb.api import send_upload
from ..models import DirectoryEmployee
from shopdb.api import require_permission
logger = logging.getLogger(__name__)
@@ -198,6 +199,7 @@ def _with_photo_url(employee):
@employees_bp.route('/resolve/<sso>', methods=['GET'])
@require_permission('employees.view')
@jwt_required()
def resolve_employee(sso):
"""Resolve one SSO to a name and photo, and say HOW.
@@ -275,6 +277,7 @@ def resolve_employee(sso):
@employees_bp.route('/search', methods=['GET'])
@require_permission('employees.view')
def search_employees():
"""
Search employees by name.
@@ -327,6 +330,7 @@ def search_employees():
@employees_bp.route('/lookup/<sso>', methods=['GET'])
@require_permission('employees.view')
def lookup_employee(sso):
"""Look up a single employee by SSO."""
if not sso.isdigit():
@@ -370,6 +374,7 @@ def lookup_employee(sso):
@employees_bp.route('/lookup', methods=['GET'])
@require_permission('employees.view')
def lookup_employees():
"""
Look up multiple employees by SSO list.
@@ -429,7 +434,7 @@ def lookup_employees():
# =============================================================================
@employees_bp.route('/directory', methods=['GET'])
@jwt_required(optional=True)
@require_permission('employees.view')
def list_directory():
"""Full self-hosted directory (for the management page)."""
guard = _require_selfhosted()

View File

@@ -51,6 +51,18 @@ class EmployeesPlugin(BasePlugin):
api_prefix=self._manifest.get('api_prefix', '/api/employees'),
)
def get_permissions(self) -> List:
"""Reading the staff directory.
The directory is names, SSOs, photographs and who reports to whom. It
was reachable with no account at all, which is how a permissions
question ends up answered by nobody. One read permission: the write
side of this plugin is an import, run from the CLI.
"""
return [
('employees.view', 'View the employee directory', 'employees'),
]
def get_blueprint(self) -> Optional[Blueprint]:
"""Return Flask Blueprint with API routes."""
return employees_bp

View File

@@ -77,7 +77,7 @@ def _search_clause(search):
@knowledgebase_bp.route('', methods=['GET'])
@jwt_required(optional=True)
@require_permission('kb.view')
def list_articles():
"""List all knowledge base articles."""
page, per_page = get_pagination_params(request)
@@ -142,7 +142,7 @@ def list_articles():
@knowledgebase_bp.route('/stats', methods=['GET'])
@jwt_required(optional=True)
@require_permission('kb.view')
def get_stats():
"""Get knowledge base statistics."""
# Counted over the same set the list shows. A total that includes articles
@@ -158,7 +158,7 @@ def get_stats():
@knowledgebase_bp.route('/<int:link_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('kb.view')
def get_article(link_id: int):
"""Get a single knowledge base article."""
article = db.session.get(KnowledgeBase, link_id)

View File

@@ -17,7 +17,7 @@ machines_bp = Blueprint('machines', __name__)
# =============================================================================
@machines_bp.route('/types', methods=['GET'])
@jwt_required(optional=True)
@require_permission('machines.view')
def list_machine_types():
"""List all machine types."""
page, per_page = get_pagination_params(request)
@@ -39,7 +39,7 @@ def list_machine_types():
@machines_bp.route('/types/<int:type_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('machines.view')
def get_machine_type(type_id: int):
"""Get a single machine type."""
t = db.session.get(MachineType, type_id)
@@ -144,7 +144,7 @@ def delete_machine_type(type_id: int):
# =============================================================================
@machines_bp.route('', methods=['GET'])
@jwt_required(optional=True)
@require_permission('machines.view')
def list_machines():
"""
List all machines with filtering and pagination.
@@ -281,7 +281,7 @@ def _dualpath_partner_for(assetid):
@machines_bp.route('/<int:machine_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('machines.view')
def get_machine(machine_id: int):
"""Get a single machine item with full details."""
mach = db.session.get(Machine, machine_id)
@@ -301,7 +301,7 @@ def get_machine(machine_id: int):
@machines_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('machines.view')
def get_machine_by_asset(asset_id: int):
"""Get machine data by asset ID."""
mach = Machine.query.filter_by(assetid=asset_id).first()
@@ -525,7 +525,7 @@ def delete_machine(machine_id: int):
# =============================================================================
@machines_bp.route('/dashboard/summary', methods=['GET'])
@jwt_required(optional=True)
@require_permission('machines.view')
def dashboard_summary():
"""Get machine dashboard summary data."""
# Dualpath single-machine collapse: hide the secondary bay from counts so a

View File

@@ -49,7 +49,7 @@ def _merged(tool, today=None):
# =============================================================================
@measuringtools_bp.route('/types', methods=['GET'])
@jwt_required(optional=True)
@require_permission('measuringtools.view')
def list_types():
"""List measuring-tool types."""
page, per_page = get_pagination_params(request)
@@ -64,7 +64,7 @@ def list_types():
@measuringtools_bp.route('/types/<int:type_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('measuringtools.view')
def get_type(type_id: int):
"""Get one measuring-tool type."""
tool_type = db.session.get(MeasuringToolType, type_id)
@@ -150,7 +150,7 @@ def delete_type(type_id: int):
# =============================================================================
@measuringtools_bp.route('', methods=['GET'])
@jwt_required(optional=True)
@require_permission('measuringtools.view')
def list_tools():
"""List measuring tools with filters + pagination.
@@ -208,7 +208,7 @@ def list_tools():
@measuringtools_bp.route('/<int:tool_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('measuringtools.view')
def get_tool(tool_id: int):
"""Get one measuring tool (asset core merged with the extension)."""
tool = db.session.get(MeasuringTool, tool_id)
@@ -219,7 +219,7 @@ def get_tool(tool_id: int):
@measuringtools_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('measuringtools.view')
def get_tool_by_asset(asset_id: int):
"""Get a measuring tool by its core asset id."""
tool = MeasuringTool.query.filter_by(assetid=asset_id).first()
@@ -371,7 +371,7 @@ def delete_tool(tool_id: int):
# =============================================================================
@measuringtools_bp.route('/map-overlay', methods=['GET'])
@jwt_required(optional=True)
@require_permission('measuringtools.view')
def map_overlay():
"""Calibration-status overlay for active measuring tools.
@@ -402,7 +402,7 @@ def map_overlay():
# =============================================================================
@measuringtools_bp.route('/report/calibration', methods=['GET'])
@jwt_required(optional=True)
@require_permission('measuringtools.view')
def calibration_report():
"""Counts + lists bucketed by derived calibration status."""
today = date.today()

View File

@@ -47,7 +47,7 @@ def _upsert_primary_ip(asset, ip):
# =============================================================================
@network_bp.route('/types', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def list_network_device_types():
"""List all network device types."""
page, per_page = get_pagination_params(request)
@@ -69,7 +69,7 @@ def list_network_device_types():
@network_bp.route('/types/<int:type_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def get_network_device_type(type_id: int):
"""Get a single network device type."""
t = db.session.get(NetworkDeviceType, type_id)
@@ -176,7 +176,7 @@ def delete_network_device_type(type_id: int):
# =============================================================================
@network_bp.route('', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def list_network_devices():
"""
List all network devices with filtering and pagination.
@@ -284,7 +284,7 @@ def list_network_devices():
@network_bp.route('/<int:device_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def get_network_device(device_id: int):
"""Get a single network device with full details."""
netdev = db.session.get(NetworkDevice, device_id)
@@ -304,7 +304,7 @@ def get_network_device(device_id: int):
@network_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def get_network_device_by_asset(asset_id: int):
"""Get network device data by asset ID."""
netdev = NetworkDevice.query.filter_by(assetid=asset_id).first()
@@ -324,7 +324,7 @@ def get_network_device_by_asset(asset_id: int):
@network_bp.route('/by-hostname/<hostname>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def get_network_device_by_hostname(hostname: str):
"""Get network device by hostname."""
netdev = NetworkDevice.query.filter_by(hostname=hostname).first()
@@ -583,7 +583,7 @@ def delete_network_device(device_id: int):
# =============================================================================
@network_bp.route('/dashboard/summary', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def dashboard_summary():
"""Get network device dashboard summary data."""
# Total active network devices
@@ -631,7 +631,7 @@ def dashboard_summary():
# =============================================================================
@network_bp.route('/vlans', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def list_vlans():
"""List all VLANs with filtering and pagination."""
page, per_page = get_pagination_params(request)
@@ -664,7 +664,7 @@ def list_vlans():
@network_bp.route('/vlans/<int:vlan_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def get_vlan(vlan_id: int):
"""Get a single VLAN with its subnets."""
vlan = db.session.get(VLAN, vlan_id)
@@ -806,7 +806,7 @@ def delete_vlan(vlan_id: int):
# =============================================================================
@network_bp.route('/subnets', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def list_subnets():
"""List all subnets with filtering and pagination."""
page, per_page = get_pagination_params(request)
@@ -847,7 +847,7 @@ def list_subnets():
@network_bp.route('/subnets/<int:subnet_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def get_subnet(subnet_id: int):
"""Get a single subnet plus the network devices whose primary IP falls in
its CIDR range."""

View File

@@ -239,6 +239,7 @@ def _employee_picture(sso):
# =============================================================================
@notifications_bp.route('/types', methods=['GET'])
@require_permission('notifications.view')
def list_notification_types():
"""List all notification types."""
page, per_page = get_pagination_params(request)
@@ -333,6 +334,7 @@ def update_notification_type(type_id: int):
# =============================================================================
@notifications_bp.route('', methods=['GET'])
@require_permission('notifications.view')
def list_notifications():
"""
List all notifications with filtering and pagination.
@@ -388,6 +390,7 @@ def list_notifications():
@notifications_bp.route('/<int:notification_id>', methods=['GET'])
@require_permission('notifications.view')
def get_notification(notification_id: int):
"""Get a single notification."""
n = db.session.get(Notification, notification_id)
@@ -551,6 +554,7 @@ def delete_notification(notification_id: int):
# =============================================================================
@notifications_bp.route('/active', methods=['GET'])
@require_permission('notifications.view')
def get_active_notifications():
"""
Get currently active notifications for display.
@@ -581,6 +585,7 @@ def get_active_notifications():
@notifications_bp.route('/calendar', methods=['GET'])
@require_permission('notifications.view')
def get_calendar_events():
"""
Get notifications in FullCalendar event format.
@@ -619,6 +624,7 @@ def get_calendar_events():
@notifications_bp.route('/dashboard/summary', methods=['GET'])
@require_permission('notifications.view')
def dashboard_summary():
"""Get notifications dashboard summary."""
now = datetime.now(timezone.utc).replace(tzinfo=None)
@@ -869,6 +875,7 @@ def get_shopfloor_notifications():
@notifications_bp.route('/dashboard/active', methods=['GET'])
@require_permission('notifications.view')
@jwt_required()
def dashboard_active():
"""Notifications currently showing on the floor.

View File

@@ -542,7 +542,7 @@ def _csv_response(rows, columns, filename):
@printedparts_bp.route('/reports/stock', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printedparts.view')
def report_stock():
"""Stock levels with low-stock flags and the cache-vs-ledger reconcile.
@@ -576,7 +576,7 @@ def report_stock():
@printedparts_bp.route('/reports/consumption', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printedparts.view')
def report_consumption():
"""Takes per item, optionally bounded by ?days=<n> (default 30)."""
days = request.args.get('days', 30, type=int)
@@ -604,7 +604,7 @@ def report_consumption():
@printedparts_bp.route('/reports/by-person', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printedparts.view')
def report_by_person():
"""Takes grouped by employee, optionally bounded by ?days=<n> (default 30)."""
days = request.args.get('days', 30, type=int)
@@ -722,7 +722,7 @@ def upload_item_file(item_id: int):
@printedparts_bp.route('/files/<int:file_id>/download', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printedparts.view')
def download_item_file(file_id: int):
"""Download a revision under its original filename."""
record = db.session.get(PrintedItemFile, file_id)

View File

@@ -30,7 +30,7 @@ printers_asset_bp = Blueprint('printers_asset', __name__)
# =============================================================================
@printers_asset_bp.route('/types', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def list_printer_types():
"""List all printer types."""
page, per_page = get_pagination_params(request)
@@ -52,7 +52,7 @@ def list_printer_types():
@printers_asset_bp.route('/types/<int:type_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def get_printer_type(type_id: int):
"""Get a single printer type."""
t = db.session.get(PrinterType, type_id)
@@ -147,7 +147,7 @@ def delete_printer_type(type_id: int):
# =============================================================================
@printers_asset_bp.route('/drivers', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def list_drivers():
"""List printer drivers. ?active=false includes inactive ones."""
query = PrinterDriver.query
@@ -207,7 +207,7 @@ def delete_driver(driver_id):
# =============================================================================
@printers_asset_bp.route('', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def list_printers():
"""
List all printers with filtering and pagination.
@@ -964,7 +964,7 @@ def printers_for_host(hostname: str):
@printers_asset_bp.route('/assignments/for-asset/<int:asset_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def get_asset_printer_assignment(asset_id: int):
"""What THIS asset is assigned, without inheritance.
@@ -1778,7 +1778,7 @@ def seed_assignment_from_observed(asset_id: int):
@printers_asset_bp.route('/<int:printer_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def get_printer(printer_id: int):
"""Get a single printer with full details."""
printer = db.session.get(Printer, printer_id)
@@ -1811,7 +1811,7 @@ def get_printer(printer_id: int):
@printers_asset_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def get_printer_by_asset(asset_id: int):
"""Get printer data by asset ID."""
printer = Printer.query.filter_by(assetid=asset_id).first()
@@ -2043,7 +2043,7 @@ def delete_printer(printer_id: int):
# =============================================================================
@printers_asset_bp.route('/<int:printer_id>/supplies', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def get_printer_supplies(printer_id: int):
"""Get supply levels from Zabbix (real-time lookup)."""
printer = db.session.get(Printer, printer_id)
@@ -2238,7 +2238,7 @@ def _get_low_supplies_data():
@printers_asset_bp.route('/lowsupplies', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def low_supplies():
"""Get printers with low or critical supply levels."""
data = _get_low_supplies_data()
@@ -2246,7 +2246,7 @@ def low_supplies():
@printers_asset_bp.route('/lookup', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def printer_lookup():
"""Find a printer by IP or FQDN. Parity with the classic printerlookup.asp.
@@ -2306,7 +2306,7 @@ def refresh_supplies_cache():
# =============================================================================
@printers_asset_bp.route('/dashboard/summary', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def dashboard_summary():
"""Get printer dashboard summary data."""
# Total active printers
@@ -2380,7 +2380,7 @@ def _validate_supply_payload(data):
@printers_asset_bp.route('/supplies/meta', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def supplies_meta():
"""Allowed values for supply type, color, and capacity tier (for the UI)."""
return success_response({
@@ -2391,7 +2391,7 @@ def supplies_meta():
@printers_asset_bp.route('/models', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def list_supply_models():
"""List models with a supply count, for the supply-management picker.
@@ -2450,7 +2450,7 @@ def list_supply_models():
@printers_asset_bp.route('/models/<int:modelnumberid>/supplies', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def list_model_supplies(modelnumberid: int):
"""List all supplies mapped to a model."""
model = db.session.get(Model, modelnumberid)
@@ -2667,7 +2667,7 @@ def dashboard_supplies():
# =============================================================================
@printers_asset_bp.route('/supplies/forecast', methods=['GET'])
@jwt_required(optional=True)
@require_permission('printers.view')
def supplies_forecast():
"""Days-to-empty and replacement counts per printer.

View File

@@ -257,7 +257,7 @@ def _log_to_dict(cur, row):
# =============================================================================
@usb_bp.route('', methods=['GET'])
@jwt_required(optional=True)
@require_permission('usb.view')
def list_usb_devices():
"""List USB devices with checkout status.
@@ -306,7 +306,7 @@ def list_usb_devices():
@usb_bp.route('/<device_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('usb.view')
def get_usb_device(device_id):
"""Get a single device plus its last 20 check-in/out log rows."""
if _usb_selfhosted():
@@ -595,7 +595,7 @@ def checkin_device(device_id):
# =============================================================================
@usb_bp.route('/<device_id>/history', methods=['GET'])
@jwt_required(optional=True)
@require_permission('usb.view')
def get_device_history(device_id):
"""Paginated check-in/out log for one device."""
if _usb_selfhosted():
@@ -625,7 +625,7 @@ def get_device_history(device_id):
@usb_bp.route('/checkouts', methods=['GET'])
@jwt_required(optional=True)
@require_permission('usb.view')
def list_all_checkouts():
"""List check-out log rows.
@@ -676,7 +676,7 @@ def list_all_checkouts():
@usb_bp.route('/checkouts/active', methods=['GET'])
@jwt_required(optional=True)
@require_permission('usb.view')
def list_active_checkouts():
"""List the latest check-out log row for every currently checked-out device.

View File

@@ -140,7 +140,7 @@ def _apply_links(warranty, assetids):
# =============================================================================
@warranty_bp.route('', methods=['GET'])
@jwt_required(optional=True)
@require_permission('warranty.view')
def list_warranties():
"""List warranties. Filters: ?status=, ?assetid=, ?active=false."""
query = Warranty.query
@@ -174,7 +174,7 @@ def list_warranties():
@warranty_bp.route('/asset/<int:assetid>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('warranty.view')
def warranties_for_asset(assetid):
"""Warranties covering one asset (for the asset-detail panel)."""
links = WarrantyAsset.query.filter_by(assetid=assetid).all()
@@ -188,7 +188,7 @@ def warranties_for_asset(assetid):
@warranty_bp.route('/<int:warrantyid>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('warranty.view')
def get_warranty(warrantyid):
warranty = db.session.get(Warranty, warrantyid)
if not warranty:
@@ -395,7 +395,7 @@ def sync_dell():
# =============================================================================
@warranty_bp.route('/report', methods=['GET'])
@jwt_required(optional=True)
@require_permission('warranty.view')
def warranty_report():
"""Counts + lists bucketed by derived status."""
today = date.today()