CMMC removable-media tracking is an audit obligation, and answering "who had this drive, and when" meant paging through the check-in/out list by hand. GET /api/usb/reports/audit returns the events - a take and a return are separate rows, because a count cannot answer the question an audit actually asks - with summary figures riding along for the people who only want the shape of it, and a CSV for the people who want it in a spreadsheet. The window is named rather than a day count: today, last month, six months, a year, all time. Nobody should have to work out that a year is 365. Both directory modes answer in one voice. The external path reads the cmmc_usb checkinoutlog; self-hosted expands one usbcheckouts row into its two ends, so a returned drive is distinguishable from one still out, and the window filter runs in SQL against either end of the loan - a drive taken before the window and returned inside it is a check-in the window has to show. Rows come back from the mode-specific half; the CSV, the summary and the response shape are built once in the route. Ordering is total in both modes. Timestamp alone ties for two events in the same second, and the same query then returns them in a different order each run, which reads as the log being unstable.
201 lines
8.3 KiB
Python
201 lines
8.3 KiB
Python
"""USB plugin main class."""
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import List, Dict, Optional, Type
|
|
|
|
from flask import Flask, Blueprint
|
|
|
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
|
|
|
from .models import USBDevice, USBDeviceType, USBCheckout
|
|
from .api import usb_bp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class USBPlugin(BasePlugin):
|
|
"""
|
|
USB plugin - manages USB device tracking and checkouts.
|
|
|
|
Standalone plugin for tracking USB flash drives, external drives,
|
|
and other portable storage devices with checkout/checkin functionality.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._manifest = self._load_manifest()
|
|
|
|
def _load_manifest(self) -> Dict:
|
|
"""Load plugin manifest from JSON file."""
|
|
manifest_path = Path(__file__).parent / 'manifest.json'
|
|
if manifest_path.exists():
|
|
with open(manifest_path, 'r') as f:
|
|
return json.load(f)
|
|
return {}
|
|
|
|
@property
|
|
def meta(self) -> PluginMeta:
|
|
"""Return plugin metadata."""
|
|
return PluginMeta(
|
|
name=self._manifest.get('name', 'usb'),
|
|
version=self._manifest.get('version', '1.0.0'),
|
|
description=self._manifest.get('description', 'USB device checkout management'),
|
|
author=self._manifest.get('author', 'ShopDB Team'),
|
|
dependencies=self._manifest.get('dependencies', []),
|
|
core_version=self._manifest.get('core_version', '>=1.0.0'),
|
|
api_prefix=self._manifest.get('api_prefix', '/api/usb'),
|
|
)
|
|
|
|
def get_blueprint(self) -> Optional[Blueprint]:
|
|
"""Return Flask Blueprint with API routes."""
|
|
return usb_bp
|
|
|
|
def get_models(self) -> List[Type]:
|
|
"""Return list of SQLAlchemy model classes."""
|
|
return [USBDeviceType, USBDevice, USBCheckout]
|
|
|
|
def get_provisioning_note(self) -> Optional[Dict]:
|
|
return {
|
|
'mode_setting': 'usb_directory_mode',
|
|
'tables': ['usbdevicetypes', 'usbdevices', 'usbcheckouts'],
|
|
'note': ('Enabling this creates USB tracking tables in the shopdb '
|
|
'database (usbdevicetypes, usbdevices, usbcheckouts) for CMMC '
|
|
'removable-media check-in/out. Devices, check-in/out events, '
|
|
'and per-event log ids live here. Planned build-out: a USB-ID '
|
|
'standard, overdue-device email reminders, and DLP/Fabric '
|
|
'approval integration (see the captured design notes). A site '
|
|
'with an existing cmmc_usb database can use external mode '
|
|
'instead.'),
|
|
'docs': 'plugins/usb/README.md',
|
|
}
|
|
|
|
def get_reports(self) -> List[Dict]:
|
|
"""Removable-media audit.
|
|
|
|
CMMC tracking is an audit obligation, and the only way to answer "who
|
|
had which drive, when" was to page through the check-in/out list by
|
|
hand. The window covers a shift, a periodic review, or everything ever
|
|
recorded.
|
|
"""
|
|
return [
|
|
{
|
|
'id': 'usb-audit',
|
|
'name': 'USB Check-in / Check-out Audit',
|
|
'description': 'Every take and return over a chosen window - '
|
|
'day, month, six months, year or all time - with '
|
|
'who, which drive and when',
|
|
'category': 'usage',
|
|
'endpoint': '/api/usb/reports/audit',
|
|
},
|
|
]
|
|
|
|
def get_settings_cards(self) -> List[Dict]:
|
|
"""Somewhere to actually turn the kiosk on.
|
|
|
|
The kiosk page tells an administrator to enable it in Settings under
|
|
USB. Without this card that place does not exist, and the instruction
|
|
is a dead end - the same trap the backups plugin shipped with, where
|
|
settings existed but only a shell could reach them.
|
|
"""
|
|
return [
|
|
{
|
|
'group': 'USB Devices',
|
|
'to': '/settings/usb',
|
|
'icon': 'usb',
|
|
'title': 'USB Settings',
|
|
'description': 'Logged-out kiosk for taking and returning '
|
|
'drives, and whether somebody may return a drive '
|
|
'checked out to another person',
|
|
'position': 45,
|
|
},
|
|
]
|
|
|
|
def get_settings_defaults(self) -> List[Dict]:
|
|
"""Kiosk settings.
|
|
|
|
The kiosk is an UNAUTHENTICATED write, so it ships off and a site turns
|
|
it on deliberately. Opening that door is a decision about the door.
|
|
"""
|
|
return [
|
|
{
|
|
'key': 'usb_kiosk_enabled',
|
|
'value': 'false',
|
|
'valuetype': 'boolean',
|
|
'category': 'usb',
|
|
# Public: the kiosk page runs logged out and has to be able to
|
|
# tell "turned off" from "broken" before anyone signs in.
|
|
'public': True,
|
|
'description': 'Allow check-out and check-in from the logged-out '
|
|
'USB kiosk screen (/usb-kiosk). Off by default: the '
|
|
'kiosk writes without a login, attributing every '
|
|
'action to a scanned badge instead. It can only move '
|
|
'a device between checked-out and checked-in - it '
|
|
'cannot add, edit, retire or list devices.',
|
|
},
|
|
{
|
|
'key': 'usb_kiosk_allow_return_for_others',
|
|
'value': 'true',
|
|
'valuetype': 'boolean',
|
|
'category': 'usb',
|
|
'public': True,
|
|
'description': 'At the kiosk, allow somebody to return a device '
|
|
'checked out to a DIFFERENT person after confirming. '
|
|
'The returning badge is what gets recorded, so the '
|
|
'log shows who actually handed it back. Turn this '
|
|
'off to refuse any mismatch outright - a device left '
|
|
'behind by someone on leave then needs a signed-in '
|
|
'user to return it.',
|
|
},
|
|
]
|
|
|
|
def get_config_schema(self) -> List[Dict]:
|
|
"""CMMC USB check-in/out database connection. Host/name/user are settings
|
|
the wizard can edit; the password stays in .env (emitted, not stored)."""
|
|
return [
|
|
{'key': 'cmmc_usb_db_host', 'label': 'USB DB host', 'type': 'text',
|
|
'secret': False, 'default': 'localhost',
|
|
'help': 'This DB must expose devices / checkinoutlog / users tables '
|
|
'(or views). See plugins/usb/README.md.'},
|
|
{'key': 'cmmc_usb_db_name', 'label': 'USB DB name', 'type': 'text',
|
|
'secret': False, 'default': 'cmmc_usb'},
|
|
{'key': 'cmmc_usb_db_user', 'label': 'USB DB user', 'type': 'text',
|
|
'secret': False},
|
|
{'key': 'cmmc_usb_db_password', 'label': 'USB DB password', 'type': 'password',
|
|
'secret': True, 'envvar': 'CMMC_USB_DB_PASSWORD',
|
|
'help': 'Stored in .env, not the database. The wizard shows the line to paste.'},
|
|
]
|
|
|
|
def init_app(self, app: Flask, db_instance) -> None:
|
|
"""Initialize plugin with Flask app."""
|
|
logger.info(f"USB plugin initialized (v{self.meta.version})")
|
|
|
|
def on_install(self, app: Flask) -> None:
|
|
"""Called when plugin is installed."""
|
|
logger.info("USB plugin installed")
|
|
|
|
def on_uninstall(self, app: Flask) -> None:
|
|
"""Called when plugin is uninstalled."""
|
|
logger.info("USB plugin uninstalled")
|
|
|
|
def get_navigation_items(self) -> List[Dict]:
|
|
"""Return navigation menu items."""
|
|
return [
|
|
{
|
|
'name': 'USB Devices',
|
|
'icon': 'usb',
|
|
'route': '/usb',
|
|
'position': 25,
|
|
'section': 'assets',
|
|
},
|
|
]
|
|
|
|
def get_permissions(self) -> List:
|
|
"""Return the RBAC permissions this plugin owns."""
|
|
return [
|
|
('usb.view', 'View USB devices', 'usb'),
|
|
('usb.create', 'Create USB devices', 'usb'),
|
|
('usb.edit', 'Edit USB devices', 'usb'),
|
|
('usb.delete', 'Delete USB devices', 'usb'),
|
|
]
|