Admit kiosk callers by network position, not only by device token
The device token assumes a handful of fixed screens an administrator provisions once. That is not how these kiosks are used: most people open the page on their own PC, and there is no login, so per-browser provisioning would mean touching every PC on the floor and losing it again on the next reimage or cleared profile. A screen in that state did not look refused either - the USB kiosk rendered a working page whose every call answered 404. Both kiosks now admit a caller on either ground, the same shape the GE-Enforce client endpoints already use: the per-device token, or a source address inside an allowlist. Wall screens keep the token and are not confined to the allowlist; everyone else is admitted by where they are on the network. Still gated on the site's enable flag, so nothing opens by itself, and both allowlists ship empty - token-only until a site enters its ranges. - shopdb.api gains ip_in_cidrs and request_ip_allowlisted (contract 0.35.0). On the surface because both plugins need the identical gate, and a CIDR parser copied into two of them is where a fail-open creeps in later. They fail closed on an empty setting, an unparseable caller address and a malformed entry, and they read remote_addr rather than raw X-Forwarded-For - so the IIS rewrite rule is what makes them meaningful, which the module says at length. - Separate settings per kiosk, usb_kiosk_allowed_cidrs and printedparts_kiosk_allowed_cidrs. One must not grant the other, for the same reason the tokens are separate. - Both settings pages get a picker listing the network plugin's subnets with their VLANs. It stores resolved CIDR strings, not subnet ids: the setting has to work at a site that does not run that plugin and must not break when an inventory row is deleted. Entries matching no subnet are shown rather than hidden, since a stale one left by an inventory edit looks exactly like a hand-typed range. - USBKiosk says it was not admitted instead of pretending to work, which PartsKiosk already did. - The USB settings page keyed text-vs-boolean off the token alone. Adding a second text setting to the boolean path would have saved it as the word 'true' - the bug that replaced the kiosk token and 404'd every provisioned screen. Both load and save now key off a list, with a spec on it. The trade, stated plainly in the admin UI: anyone on an allowed network can check a drive in or out against a badge number they guess, and the record carries that person's name. Badges are attribution, not authorization. Keeping the ranges narrow is what bounds it; login is the alternative and was ruled out because these users do not sign in.
This commit is contained in:
@@ -39,7 +39,7 @@ from shopdb.api import (
|
||||
get_pagination_params,
|
||||
require_permission,
|
||||
)
|
||||
from shopdb.api import Setting
|
||||
from shopdb.api import Setting, request_ip_allowlisted
|
||||
|
||||
from . import selfhosted
|
||||
|
||||
@@ -743,22 +743,14 @@ def _kiosk_enabled():
|
||||
return _kiosk_setting('usb_kiosk_enabled', False)
|
||||
|
||||
|
||||
def _kiosk_open():
|
||||
"""True when the kiosk is on for this site AND the caller has its token.
|
||||
|
||||
The flag alone was the whole gate, and a flag is a decision, not a
|
||||
credential: with the kiosk on, anyone who could reach the app could move a
|
||||
device between checked-out and checked-in and have it recorded against a
|
||||
badge they guessed. A badge is a short numeric SSO, so it is attribution,
|
||||
never authorization - the same reasoning that closed the parts kiosk.
|
||||
def _kiosk_token_presented():
|
||||
"""True when the caller presents the configured device token.
|
||||
|
||||
The token is per-DEVICE. A kiosk is a browser on a screen by the cabinet, so
|
||||
a token served inside the page would be readable by anyone who could load
|
||||
it. An admin opens the screen once as /usb-kiosk?token=..., the page keeps
|
||||
it in that browser and sends it as X-Kiosk-Token.
|
||||
"""
|
||||
if not _kiosk_enabled():
|
||||
return False
|
||||
expected = (Setting.get('usb_kiosk_token') or '').strip()
|
||||
presented = (request.headers.get('X-Kiosk-Token') or '').strip()
|
||||
if not expected or not presented:
|
||||
@@ -768,6 +760,46 @@ def _kiosk_open():
|
||||
return hmac.compare_digest(presented, expected)
|
||||
|
||||
|
||||
def _kiosk_ip_allowlisted():
|
||||
"""True when the caller sits in the configured kiosk network allowlist.
|
||||
|
||||
Why this exists alongside the token: the token model assumes a handful of
|
||||
fixed screens provisioned once. Most people reach this kiosk from their own
|
||||
PC instead, and there is no login - so per-browser provisioning would mean
|
||||
provisioning every PC on the floor, and losing it on every reimage or
|
||||
cleared profile. Network position is what those callers actually have in
|
||||
common.
|
||||
|
||||
Fail-closed behaviour and the IIS rewrite-rule dependency that makes the
|
||||
caller address trustworthy are documented on request_ip_allowlisted.
|
||||
"""
|
||||
return request_ip_allowlisted(
|
||||
request, Setting.get('usb_kiosk_allowed_cidrs'))
|
||||
|
||||
|
||||
def _kiosk_open():
|
||||
"""True when the kiosk is on for this site AND the caller is admitted.
|
||||
|
||||
The flag alone was the whole gate, and a flag is a decision, not a
|
||||
credential: with the kiosk on, anyone who could reach the app could move a
|
||||
device between checked-out and checked-in and have it recorded against a
|
||||
badge they guessed. A badge is a short numeric SSO, so it is attribution,
|
||||
never authorization - the same reasoning that closed the parts kiosk.
|
||||
|
||||
Two ways in, either sufficient, mirroring what the GE-Enforce client
|
||||
endpoints already do:
|
||||
1. the per-device token, for unattended screens by the cabinet, and
|
||||
2. a source address inside usb_kiosk_allowed_cidrs, for the people who
|
||||
open this page on their own PC and never log in.
|
||||
|
||||
Both are still gated on usb_kiosk_enabled, so the door stays shut until a
|
||||
site opens it deliberately.
|
||||
"""
|
||||
if not _kiosk_enabled():
|
||||
return False
|
||||
return _kiosk_token_presented() or _kiosk_ip_allowlisted()
|
||||
|
||||
|
||||
def _kiosk_disabled_response():
|
||||
# 404, not 403: when the kiosk is off these routes should not read as
|
||||
# "there is a door here and you may not use it".
|
||||
|
||||
Reference in New Issue
Block a user