Add GE-Enforce P2 admin CRUD API: scopes, entries, reorder, simulate, publish

Full HTTP admin surface behind the manifest editor (geenforce.manage for edits,
geenforce.publish for shipping):

- Scopes: POST/GET/PUT/DELETE /scopes[/<id>] (create imaging PC types, edit the
  ComputerType/MeasuringToolType mapping + metadata, delete).
- Entries: POST /scopes/<id>/entries, PUT/DELETE /entries/<id>. Payloads use the
  manifest Applications[] shape; populate_entry (refactored out of build_entry)
  updates an entry in place, resetting omitted fields and replacing children.
- Reorder: PUT /scopes/<id>/entries/reorder enforces the ordering contract
  (body must list exactly the scope's entry ids).
- Simulate: GET /scopes/<id>/simulate?pctype&subtype&hostname&machinenumber&
  cmmversion returns which entries apply and which filter excluded the rest,
  reusing the engine-mirror filters. The "what would this PC get" tool.
- Publish lifecycle: POST /scopes/<id>/publish (records publishedby from JWT),
  GET /scopes/<id>/versions, GET .../versions/<n> (frozen manifest),
  POST /scopes/<id>/rollback.

Entry type validated against ENTRY_TYPES; 8 CRUD tests. JWT+permission gated so
the authz sweep covers them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-12 17:13:27 -04:00
parent 6dc31c6149
commit d157502d5b
3 changed files with 499 additions and 22 deletions

View File

@@ -24,33 +24,40 @@ _KEY_TO_ATTR = {mk: attr for attr, mk in _SCALAR_FIELDS}
_FLAG_TO_ATTR = {mk: attr for attr, mk in _BOOL_FLAGS}
def build_entry(entry_dict, sortorder):
"""Build a ManifestEntry (+ children) from one Applications[] entry."""
entry = ManifestEntry(sortorder=sortorder,
name=entry_dict.get('Name'),
entrytype=entry_dict.get('Type'))
if '_comment' in entry_dict:
entry.comment = entry_dict['_comment']
# Scalar model attributes that a populate resets (so an update clears fields the
# new payload omits). name/entrytype are required and set explicitly.
_RESETTABLE_ATTRS = [attr for _, attr in _SCALAR_FIELDS
if attr not in ('name', 'entrytype')] + ['comment', 'regvalue']
def populate_entry(entry, entry_dict):
"""Set every field + child on an existing (or new) ManifestEntry from one
Applications[] entry dict. Existing children are replaced. Returns entry."""
entry.name = entry_dict.get('Name')
entry.entrytype = entry_dict.get('Type')
entry.comment = entry_dict.get('_comment')
for attr in _RESETTABLE_ATTRS:
if attr in ('comment', 'regvalue'):
continue
setattr(entry, attr, None)
for key, attr in _KEY_TO_ATTR.items():
if attr in ('name', 'entrytype'):
continue
if key in entry_dict and entry_dict[key] is not None:
setattr(entry, attr, entry_dict[key])
# RegValue stored as its raw JSON literal so DWord vs string typing survives.
if 'RegValue' in entry_dict:
entry.regvalue = json.dumps(entry_dict['RegValue'])
entry.regvalue = (json.dumps(entry_dict['RegValue'])
if 'RegValue' in entry_dict else None)
for key, attr in _FLAG_TO_ATTR.items():
if entry_dict.get(key):
setattr(entry, attr, True)
# Multi-value filters -> child rows (preserve order).
for i, value in enumerate(entry_dict.get('PCTypes') or []):
entry.pctypes.append(ManifestEntryPcType(sortorder=i, pctypevalue=value))
for i, value in enumerate(entry_dict.get('TargetHostnames') or []):
entry.hostnames.append(
ManifestEntryHostname(sortorder=i, hostnamepattern=value))
for i, value in enumerate(entry_dict.get('TargetMachineNumbers') or []):
entry.machinenumbers.append(
ManifestEntryMachineNumber(sortorder=i, machinenumber=str(value)))
setattr(entry, attr, bool(entry_dict.get(key)))
# Multi-value filters -> child rows (replace, preserve order).
entry.pctypes = [ManifestEntryPcType(sortorder=i, pctypevalue=value)
for i, value in enumerate(entry_dict.get('PCTypes') or [])]
entry.hostnames = [ManifestEntryHostname(sortorder=i, hostnamepattern=value)
for i, value in enumerate(entry_dict.get('TargetHostnames') or [])]
entry.machinenumbers = [
ManifestEntryMachineNumber(sortorder=i, machinenumber=str(value))
for i, value in enumerate(entry_dict.get('TargetMachineNumbers') or [])]
# InUseCheck object + Processes[].
inuse = entry_dict.get('InUseCheck')
if inuse:
@@ -62,9 +69,17 @@ def build_entry(entry_dict, sortorder):
exepath=proc.get('ExePath'),
gracefulclosetimeoutsec=proc.get('GracefulCloseTimeoutSec')))
entry.inusecheck = check
else:
entry.inusecheck = None
return entry
def build_entry(entry_dict, sortorder):
"""Build a new ManifestEntry (+ children) from one Applications[] entry."""
entry = ManifestEntry(sortorder=sortorder)
return populate_entry(entry, entry_dict)
def build_scope(scopename, phase, manifest_dict, iscommon=False):
"""Build a ManifestScope (+ entries) from a parsed manifest dict."""
scope = ManifestScope(