GE-Enforce: compliance view, inline payload upload, frontend test harness
Fleet-install compliance for app-linked manifest entries: new service compliance_for_scope + GET /geenforce/scopes/<id>/compliance count active ComputerInstalledApp rows by curated appid (null-safe when computers plugin absent). ManifestEditor gains a compliance panel. Curated appid stays shopdb metadata and never enters manifest JSON, so behavioral parity is unaffected. Inline manifest payloads: store_inline_payload (sha256, 1MB cap, payloadsource='inline') + POST/GET /geenforce/entries/<id>/payload; editor gains an upload control. Entry payload metadata surfaced in _entry_payload. Frontend test harness: extract the editor's entry-form logic into pure entryForm.js (buildEntryPayload, describeEntry, availableEntryTypes, scope gates, ...) and cover it with 45 vitest tests. ManifestEditor now imports those helpers, so the tests exercise the shipped code path (no duplication). 908 backend tests pass; vitest 45 pass; frontend build green; naming green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ SHAREROOT_SETTING = 'geenforce_share_root'
|
||||
|
||||
from ..models import (
|
||||
ManifestScope, ManifestEntry, ManifestPublishedVersion,
|
||||
ManifestEnforcementReport, ENTRY_TYPES, PHASES,
|
||||
ManifestEnforcementReport, ManifestPayload, ENTRY_TYPES, PHASES,
|
||||
)
|
||||
from ..serializer import scope_to_manifest, entry_to_dict
|
||||
from ..importer import build_entry, populate_entry
|
||||
@@ -202,6 +202,12 @@ def _entry_payload(entry):
|
||||
app = db.session.get(Application, entry.appid)
|
||||
data['appname'] = app.appname if app else None
|
||||
data.update(entry_to_dict(entry))
|
||||
# Inline-payload metadata (shopdb-only, NOT manifest keys) for the editor.
|
||||
data['payloadsource'] = entry.payloadsource
|
||||
data['payloadref'] = entry.payloadref
|
||||
data['payloadsha256'] = entry.payloadsha256
|
||||
data['haspayload'] = (entry.payloadsource == 'inline'
|
||||
and entry.payloadsha256 is not None)
|
||||
return data
|
||||
|
||||
|
||||
@@ -426,6 +432,74 @@ def simulate_scope(scopeid):
|
||||
'filtered': filtered})
|
||||
|
||||
|
||||
# -- compliance: "how much of the fleet has this app" -------------------------
|
||||
|
||||
@geenforce_bp.route('/scopes/<int:scopeid>/compliance', methods=['GET'])
|
||||
@jwt_required()
|
||||
@require_permission('geenforce.manage')
|
||||
def scope_compliance(scopeid):
|
||||
"""Fleet-install coverage per app-linked entry (from collected PC data).
|
||||
|
||||
One row per entry that carries a curated appid; installed/version-match
|
||||
counts come from the computers plugin's ComputerInstalledApp. Degrades to
|
||||
null counts (computersplugin: false) when that plugin is absent.
|
||||
"""
|
||||
scope = db.session.get(ManifestScope, scopeid)
|
||||
if not scope:
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'No such scope',
|
||||
http_code=404)
|
||||
return success_response(service.compliance_for_scope(scope))
|
||||
|
||||
|
||||
# -- inline payload upload / fetch (small scripts + configs) ------------------
|
||||
|
||||
PAYLOAD_MAX_BYTES = 1024 * 1024
|
||||
|
||||
|
||||
@geenforce_bp.route('/entries/<int:entryid>/payload', methods=['POST'])
|
||||
@jwt_required()
|
||||
@require_permission('geenforce.publish')
|
||||
def upload_entry_payload(entryid):
|
||||
"""Store an inline payload (<= 1 MB) for an entry and point the entry at it."""
|
||||
entry = db.session.get(ManifestEntry, entryid)
|
||||
if not entry:
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'No such entry', http_code=404)
|
||||
uploaded = request.files.get('file')
|
||||
if uploaded is None:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
'a file is required', http_code=400)
|
||||
rawbytes = uploaded.read()
|
||||
if not rawbytes:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
'payload is empty', http_code=400)
|
||||
if len(rawbytes) > PAYLOAD_MAX_BYTES:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
'payload exceeds 1 MB limit', http_code=400)
|
||||
service.store_inline_payload(entry, uploaded.filename,
|
||||
uploaded.mimetype, rawbytes)
|
||||
db.session.commit()
|
||||
return success_response(_entry_payload(entry), http_code=201)
|
||||
|
||||
|
||||
@geenforce_bp.route('/entries/<int:entryid>/payload', methods=['GET'])
|
||||
@jwt_required()
|
||||
@require_permission('geenforce.manage')
|
||||
def download_entry_payload(entryid):
|
||||
"""Return the stored inline payload bytes for an entry (404 if none)."""
|
||||
entry = db.session.get(ManifestEntry, entryid)
|
||||
if not entry:
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'No such entry', http_code=404)
|
||||
payload = ManifestPayload.query.filter_by(entryid=entryid).first()
|
||||
if not payload:
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'entry has no payload',
|
||||
http_code=404)
|
||||
return Response(
|
||||
payload.payloadbytes,
|
||||
mimetype=payload.contenttype or 'application/octet-stream',
|
||||
headers={'Content-Disposition':
|
||||
f'attachment; filename="{payload.filename}"'})
|
||||
|
||||
|
||||
# -- publish lifecycle (geenforce.publish) ------------------------------------
|
||||
|
||||
@geenforce_bp.route('/scopes/<int:scopeid>/publish', methods=['POST'])
|
||||
|
||||
Reference in New Issue
Block a user