One badge vocabulary, and images a browser can actually load
Two operator-visible defects and three doc corrections, closing the tail of the 2026-09-03 run. THE BADGE. The two kiosks read the same physical badges and disagreed. The parts kiosk understood both shapes a scanner emits - the bare SSO and the PayNo wrapper 0<digits>BZ - while the USB kiosk compared the raw string to an SSO, so the wrapper resolved to nobody. An operator whose keyboard-wedge scanner emits it could take a 3D-printed part and could not check out a drive, with the same badge, at two cabinets a few metres apart. The shape now lives in shopdb/utils/badges.py on the contract surface, because a plugin may not import another plugin and because a badge is a property of the site rather than of a cabinet. Both kiosks use it, and both store the DIGITS rather than the string that arrived: one row per spelling makes a custody history that cannot be joined to itself. That closed a subtler hole in the same place. str.isdigit() is true for Arabic-Indic and other decimal forms, and int() parses them, so a badge written in those characters was accepted, matched a REAL employee, and was then stored as an SSO string nothing else matches. ASCII digits only. An empty scan and an unreadable one now get different answers, because they are different problems for the person at the cabinet. THE IMAGES. /api/applications/image/<filename> and /api/models/image/<filename> required a permission, and a browser fetching <img src="..."> sends no Authorization header - so the gate did not protect the file, it guaranteed the picture never appeared. Both are ungated now, like the four sibling image routes (branding logo, map blueprint, employee photo, part picture), and registered in PUBLIC_ENDPOINTS with that reasoning. /api/printers/lookup?fqdn= never matched anything: both arguments were compared against Communication.ipaddress, so a hostname lookup 404d on printers whose hostname was on the record. An FQDN now matches the hostname, with a fallback to the short name. The slide manager gained the per-slide hold time. The PATCH route and the feed have always carried `seconds`; nothing called it, so the one documented per-slide control was curl-only. Docs: API-REFERENCE listed the employee lookups and the whole notifications surface as unauthenticated when they answer 401, and called both kiosks "deliberately open" when they are off by default and refuse anything without the site's device token. The installer download says applications.download, which is what the code requires. api-inventory.json said printers/lookup was jwt-optional. Fourteen tests. The badge rule verified by planting the old per-kiosk behaviour: three fail, one in each kiosk. Scope: product
This commit is contained in:
@@ -44,6 +44,12 @@ PUBLIC_ENDPOINTS = {
|
||||
'settings.serve_branding_logo',
|
||||
'settings.serve_map_blueprint',
|
||||
'employees.serve_employee_photo',
|
||||
'applications.serve_application_image',
|
||||
# Same reasoning: a machine detail page renders the catalog model's photo
|
||||
# with <img :src="withBase(imageurl)">, which sends no Authorization
|
||||
# header. A gate here does not protect the file, it only guarantees the
|
||||
# photo never appears.
|
||||
'models.serve_model_image',
|
||||
|
||||
# API documentation: shapes, not data. The docs page loads its own script.
|
||||
'docs.docs_index', 'docs.openapi_spec', 'docs.redoc_js', 'docs.llms_txt',
|
||||
|
||||
@@ -396,3 +396,44 @@ def test_a_check_in_closes_every_open_row_for_that_device(client, kiosk, db):
|
||||
|
||||
assert _in(client, badge='270015376').status_code == 200
|
||||
assert _open_checkouts(USBDevice) == []
|
||||
|
||||
|
||||
# --- one badge vocabulary for the whole site --------------------------------
|
||||
#
|
||||
# The parts kiosk understood both shapes a scanner emits; this one took the
|
||||
# string raw and compared it to an SSO. So an operator whose keyboard-wedge
|
||||
# scanner emits the PayNo wrapper could take a 3D-printed part and could not
|
||||
# check out a drive, with the same badge, at two cabinets a few metres apart.
|
||||
#
|
||||
# The same change closed a subtler hole: str.isdigit() is true for Arabic-Indic
|
||||
# digits, so a badge written in them resolved through int() to a REAL employee
|
||||
# and was then stored as an SSO string that joins to nothing.
|
||||
|
||||
def test_the_payno_wrapper_works_at_this_kiosk_too(client, kiosk):
|
||||
from plugins.usb.models import USBCheckout
|
||||
|
||||
response = _out(client, badge='0270015376BZ')
|
||||
assert response.status_code == 200, response.get_json()
|
||||
|
||||
row = USBCheckout.query.order_by(USBCheckout.checkoutid.desc()).first()
|
||||
assert row.sso == '270015376', \
|
||||
'the custody row records the SSO, not the wrapper it arrived in'
|
||||
assert 'Dana' in (row.checkoutname or '')
|
||||
|
||||
|
||||
def test_a_badge_in_non_ascii_digits_is_refused(client, kiosk):
|
||||
"""It looks like a number to str.isdigit() and parses through int(), so it
|
||||
resolved to a real employee under an SSO nothing else matches."""
|
||||
# Written as escapes so this file stays ASCII, which the style gate
|
||||
# enforces: these are Arabic-Indic digits, U+0669 and friends.
|
||||
arabic_indic = '\u0669\u0667\u0660\u0660\u0660\u0667\u0661'
|
||||
response = _out(client, badge=arabic_indic)
|
||||
assert response.status_code == 422, response.get_json()
|
||||
|
||||
|
||||
def test_the_same_person_scanning_either_shape_is_one_holder(client, kiosk):
|
||||
"""Wrapper out, bare number back: the check-in has to recognise it as the
|
||||
same person, or the drive can never be returned by its holder."""
|
||||
assert _out(client, badge='0270015376BZ').status_code == 200
|
||||
returned = _in(client, badge='270015376')
|
||||
assert returned.status_code == 200, returned.get_json()
|
||||
|
||||
Reference in New Issue
Block a user