Remediate the 2026-09-03 feature-test findings

This commit is contained in:
2026-09-03 17:15:03 -04:00
parent cf95daf3bb
commit fd513c08eb
112 changed files with 5182 additions and 857 deletions

View File

@@ -7,7 +7,7 @@ from shopdb.api import db, Asset, AssetType, OperatingSystem, Application, Audit
from ..models import Computer, ComputerType, ComputerInstalledApp, AccessProtocol, ComputerAccess
from shopdb.api import require_permission, require_role, apply_import_timestamps
from shopdb.api import require_permission, require_role, apply_import_timestamps, intarg
computers_bp = Blueprint('computers', __name__)
@@ -258,13 +258,42 @@ def _computer_access_links(comp, domain=None):
return links
def _ip_comtype():
"""The 'IP' communication type, created if this site has none.
Both the create and the update path did `if ip_comtype:` and silently did
nothing when the row was missing - so a person typed an address, saved, got
a 200, and the address was gone. A missing reference row is a seeding gap,
not a reason to discard what somebody typed.
"""
comtype = CommunicationType.query.filter_by(comtype='IP').first()
if comtype is None:
comtype = CommunicationType(comtype='IP', description='IP address')
db.session.add(comtype)
db.session.flush()
return comtype
def _sync_access_methods(comp, data):
"""Replace a computer's enabled protocols from data['accessmethods'] (a list
of {protocolid, portoverride?}). No-op if the key is absent, so callers that
don't touch access aren't affected."""
don't touch access aren't affected.
Returns a {'old': [...], 'new': [...]} change entry when the set actually
moved, or None. The caller records it: which remote-access protocols reach
a shop-floor PC, and on which ports, is exactly the kind of change an audit
log exists to remember, and this wrote nothing at all.
"""
if 'accessmethods' not in data:
return
return None
desired = data.get('accessmethods') or []
def describe(rows):
return sorted('%s:%s' % (row.protocolid, row.portoverride or 'default')
for row in rows)
before = describe(ComputerAccess.query.filter_by(
computerid=comp.computerid).all())
ComputerAccess.query.filter_by(computerid=comp.computerid).delete()
seen = set()
for m in desired:
@@ -287,6 +316,13 @@ def _sync_access_methods(comp, data):
isactive=True,
))
db.session.flush()
after = describe(ComputerAccess.query.filter_by(
computerid=comp.computerid).all())
if before != after:
return {'old': before, 'new': after}
return None
# =============================================================================
# Computers CRUD
@@ -380,20 +416,20 @@ def list_computers():
)
# Computer type filter
if type_id := request.args.get('typeid', request.args.get('type_id')):
query = query.filter(Computer.computertypeid == int(type_id))
if (type_id := intarg('typeid', intarg('type_id'))) is not None:
query = query.filter(Computer.computertypeid == type_id)
# OS filter
if os_id := request.args.get('osid', request.args.get('os_id')):
query = query.filter(Computer.osid == int(os_id))
if (os_id := intarg('osid', intarg('os_id'))) is not None:
query = query.filter(Computer.osid == os_id)
# Location filter
if location_id := request.args.get('locationid', request.args.get('location_id')):
query = query.filter(Asset.locationid == int(location_id))
if (location_id := intarg('locationid', intarg('location_id'))) is not None:
query = query.filter(Asset.locationid == location_id)
# Business unit filter
if bu_id := request.args.get('businessunitid', request.args.get('businessunit_id')):
query = query.filter(Asset.businessunitid == int(bu_id))
if (bu_id := intarg('businessunitid', intarg('businessunit_id'))) is not None:
query = query.filter(Asset.businessunitid == bu_id)
# Shopfloor filter (by the Shopfloor computer type)
if shopfloor := request.args.get('shopfloor'):
@@ -587,7 +623,7 @@ def create_computer():
# Optional primary IP communication
if data.get('ipaddress'):
ip_comtype = CommunicationType.query.filter_by(comtype='IP').first()
ip_comtype = _ip_comtype()
if ip_comtype:
db.session.add(Communication(
assetid=asset.assetid,
@@ -596,7 +632,9 @@ def create_computer():
isprimary=True,
))
# Remote-access protocols
# Remote-access protocols. The return value is not recorded here: a create
# is already described in full by its 'created' entry, and there is no
# previous state to compare against.
_sync_access_methods(comp, data)
# Preserve legacy timestamps in import mode (no-op otherwise)
@@ -697,20 +735,27 @@ def update_computer(computer_id: int):
ip = (data.get('ipaddress') or '').strip()
primary = Communication.query.filter_by(
assetid=asset.assetid, isprimary=True).first()
# Recorded, like every other field on this form. The address a PC
# answers on is how somebody finds it later; changing it wrote no audit
# entry at all, so a machine that moved had no history of moving.
oldip = primary.ipaddress if primary else None
if (oldip or None) != (ip or None):
changes['ipaddress'] = {'old': oldip, 'new': ip or None}
if ip:
if primary:
primary.ipaddress = ip
else:
ip_comtype = CommunicationType.query.filter_by(comtype='IP').first()
if ip_comtype:
db.session.add(Communication(
assetid=asset.assetid, comtypeid=ip_comtype.comtypeid,
ipaddress=ip, isprimary=True))
db.session.add(Communication(
assetid=asset.assetid,
comtypeid=_ip_comtype().comtypeid,
ipaddress=ip, isprimary=True))
elif primary:
primary.ipaddress = None
# Remote-access protocols
_sync_access_methods(comp, data)
accesschange = _sync_access_methods(comp, data)
if accesschange:
changes['accessmethods'] = accesschange
# Audit log if there were changes
if changes: