Let a site close the legacy collector routes, once it can afford to

Turning "Shared key allowed" off refused the key on the endpoints plugin and
left four core collector routes open to it. Anything still holding the old key
could keep rewriting logged-in user, last boot time, serial number and installed
app inventory for ANY hostname, while the admin believed the shared key was off.
Data integrity only - no command execution, no credential issuance - but it is a
hole in a boundary the product advertises as closed.

It cannot simply be closed. Both GE-Enforce reporters still authenticate with
that key, so refusing it fleet-wide would stop asset and printer reporting
everywhere, and on an air-gapped site mid-migration that is the wrong order of
operations. collector.legacysharedkeyrefused therefore DEFAULTS TO TODAY'S
BEHAVIOUR, and its description names exactly what turning it on breaks and when
it should be turned on: after the reporters move to per-PC credentials, not
before.

A legacy route used with the shared key now logs a warning, so a site can find
out who still depends on the key before flipping anything. On a stock Windows
deployment that warning may name the proxy rather than the caller; the token
prefix in the same line is the identifier to go by.

The switch is seeded with the core defaults and has a control on the
authentication settings page. Undeclared, it was a row that did not exist, and
this UI has no generic key/value editor, so seeding alone would have rendered
nothing for an operator to click.

Two contract declarations corrected while in here:

notifications.alertscope was declared db.JSON in the model while the migration
that creates it ships TEXT, and those migrations state the constraint in their
own docstrings: no JSON columns. The model now matches the shipped column rather
than the schema being bent to the model.

usb and printedparts both import a name that arrived in contract 0.35.0 while
declaring core_version floors of 0.1.0 and 0.16.0, so the loader would have
accepted them on a core that cannot satisfy them - an ImportError at blueprint
registration instead of a refusal with a reason. Both floors now say what the
plugin actually needs.
This commit is contained in:
cproudlock
2026-09-18 13:30:40 -04:00
parent 7ac1ee3077
commit 2f98188feb
10 changed files with 850 additions and 16 deletions

View File

@@ -1,10 +1,46 @@
"""Notifications plugin models - adapted to existing database schema."""
import json
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
from sqlalchemy import Text
from sqlalchemy.types import TypeDecorator
from shopdb.api import db
class JSONText(TypeDecorator):
"""A dict on the Python side, TEXT in the database.
MySQL 5.6 has no JSON type and a site still runs it, so every JSON-bearing
column in this product is TEXT. Declared here rather than imported from the
computers plugin: notifications declares no dependency on computers, and a
cross-plugin import would be a hidden one.
"""
impl = Text
cache_ok = True
def process_bind_param(self, value, dialect):
if value is None:
return None
return json.dumps(value, separators=(',', ':'))
def process_result_value(self, value, dialect):
if value is None or value == '':
return None
try:
return json.loads(value)
except (TypeError, ValueError):
# Hand-edited or written before this column held JSON. Return the
# raw text rather than raising: a scope that cannot be parsed
# should be visible and fixable, and the endpoints matcher already
# treats a non-dict scope as matching nothing.
return value
_DEFAULT_TZ = 'America/New_York'
# Keys a fleet alert's alertscope may use. Must stay equal to the set the
@@ -155,7 +191,11 @@ class Notification(db.Model):
# Matches migration 0006: NOT NULL with server defaults.
alertfleet = db.Column(db.Boolean, nullable=False, default=False,
server_default=db.false())
alertscope = db.Column(db.JSON, nullable=True)
# JSONText, not db.JSON: migration 0006 ships this column as TEXT because a
# site still runs MySQL 5.6, which has no JSON type. db.JSON here meant
# create-all built a JSON column the migration path never produces, so the
# model and the shipped schema disagreed about the same column.
alertscope = db.Column(JSONText, nullable=True)
alerttimeout = db.Column(db.Integer, nullable=False, default=0,
server_default='0')

View File

@@ -5,7 +5,7 @@
"display_name": "3D Printed Parts",
"author": "",
"dependencies": ["employees"],
"core_version": ">=0.16.0,<1.0.0",
"core_version": ">=0.35.0,<1.0.0",
"api_prefix": "/api/printedparts",
"default_enabled": false
}

View File

@@ -5,7 +5,7 @@
"description": "USB device checkout management",
"author": "ShopDB Team",
"dependencies": [],
"core_version": ">=0.1.0,<1.0.0",
"core_version": ">=0.35.0,<1.0.0",
"api_prefix": "/api/usb",
"default_enabled": false
}