Say when a kiosk screen is not set up, and hand out the setup link

Two failures from the same root: an administrator cannot see the kiosk token,
and a screen that was provisioned wrongly does not say so.

The settings API masks every key whose name contains 'token', so the settings
page shows '********'. The real value is visible for one moment - after
Generate, before Save - and an administrator who copies the field at any other
time provisions a screen with the literal mask. The kiosk then answers 404,
exactly as it answers a caller it has never heard of, because the route
deliberately does not advertise itself. Nothing anywhere says what went wrong.

- Each kiosk gains a permission-gated provisioning-link route that returns the
  real setup URL, token included, to somebody who already holds settings.edit.
  The mask stays: a token has no business in an ordinary settings list. The
  copy step just stops depending on catching the one moment it is on screen.
  Revealing it writes an audit row, since that is when the value left the
  database. Both settings pages get a "Copy setup link" button, which falls
  back to showing the link when the clipboard is refused - it is, over plain
  http and in locked-down browsers.
- The USB kiosk asks the server on load whether this screen may be used, and
  says plainly when it may not. It rendered a keypad and only failed once
  somebody had typed their SSO, which reads as broken rather than as not set
  up. It cannot guess: the enabled flag is public and only says the site has a
  kiosk, and a caller admitted by network holds no token to look for, so the
  parts kiosk's localStorage check would warn exactly the callers the allowlist
  exists to serve. The probe is a badge-less session request - the gate runs
  before any badge handling, so refused is 404 and admitted is the validation
  error, with no lookup and nothing recorded. A test pins that ordering,
  because reordering it would start telling admitted screens they are not set
  up.

Verified in a browser, not only in tests: provisioning with the mask lands on
the refused screen, a correct token gets the drive list and survives a plain
revisit, and a tokenless caller inside the allowlist gets straight in.
This commit is contained in:
cproudlock
2026-09-16 15:26:44 -04:00
parent da91a29155
commit 0f56653345
6 changed files with 217 additions and 3 deletions

View File

@@ -136,3 +136,69 @@ def test_usb_allowlist_does_not_open_the_parts_kiosk(usbkiosk, client):
response = client.get('/api/printedparts/kiosk/lookup?code=ANY',
environ_base={'REMOTE_ADDR': PLANT})
assert response.status_code == 404
# -- what the page probes on load ---------------------------------------------
def test_empty_badge_probe_separates_refused_from_admitted(usbkiosk, client):
"""The kiosk page asks the server on load whether it may be used.
It cannot guess: the enabled flag is public and only says the site HAS a
kiosk, and a caller admitted by network carries no token to look for. So the
page sends a badge-less session request and reads the status.
That only works while the gate runs BEFORE badge handling - refused is 404,
admitted is the badge validation error. If someone reorders those, the page
starts telling admitted screens they are not set up, so hold it here.
"""
refused = client.post('/api/usb/kiosk/session', json={'badge': ''},
environ_base={'REMOTE_ADDR': ELSEWHERE})
assert refused.status_code == 404
_allow(usbkiosk, 'usb_kiosk_allowed_cidrs', PLANT_CIDR)
admitted = client.post('/api/usb/kiosk/session', json={'badge': ''},
environ_base={'REMOTE_ADDR': PLANT})
assert admitted.status_code != 404
assert admitted.status_code < 500
def test_the_probe_records_nothing(usbkiosk, client):
"""A page load must not leave a checkout, a log row, or a resolved badge."""
from plugins.usb.models import USBCheckout
_allow(usbkiosk, 'usb_kiosk_allowed_cidrs', PLANT_CIDR)
client.post('/api/usb/kiosk/session', json={'badge': ''},
environ_base={'REMOTE_ADDR': PLANT})
assert USBCheckout.query.count() == 0
# -- provisioning link --------------------------------------------------------
# The settings API masks any key named '*token*', so the settings page shows
# '********'. An administrator who copies that provisions a screen with the mask
# and gets a silent 404 - the failure that cost real time on 2026-09-16. These
# hold the way out: a named, permission-gated route that hands the real value to
# somebody already allowed to set it.
def test_provisioning_link_carries_the_real_token(usbkiosk, client, auth_headers):
response = client.get('/api/usb/kiosk/provisioning-link',
headers=auth_headers)
assert response.status_code == 200
url = response.get_json()['data']['url']
assert url == '/usb-kiosk?token=a-real-token'
assert '*' not in url
def test_provisioning_link_needs_permission(usbkiosk, client):
"""Anonymous callers cannot read the token out of this route."""
response = client.get('/api/usb/kiosk/provisioning-link')
assert response.status_code in (401, 403)
def test_provisioning_link_says_so_when_no_token_is_set(usbkiosk, client,
auth_headers):
Setting.query.filter_by(key='usb_kiosk_token').first().value = ''
usbkiosk.session.commit()
response = client.get('/api/usb/kiosk/provisioning-link',
headers=auth_headers)
assert response.status_code == 400
assert 'Generate' in response.get_json()['data']['error']['message']