geenforce: HTTPS payload delivery (content-addressed blob store + endpoint)

Lets share-less (Intune/local-account) PCs pull installers the manifest
references over HTTPS instead of SMB - the general capability the whole fleet
migrates toward. New ManifestBlob registry (migration 0002) with bytes on disk
at instance/geenforce/payloads/<sha256> (deduped by content); service.store_blob
+ blob_path; client-facing GET /api/geenforce/payload/<sha256> (geenforce.fetch
token, ETag=hash, serves the blob store or an inline DB payload by hash). The
serializer now emits PayloadSource/PayloadSha256/PayloadRef for http/inline
entries only (smb entries round-trip unchanged - parity green). CLI
'flask geenforce add-payload <file>' registers a blob and prints its sha256.
This is the shopdb half (B1); the PS client/engine fetch is B2.
This commit is contained in:
cproudlock
2026-07-21 10:10:59 -04:00
parent 60e2947fc7
commit b00ef72581
8 changed files with 268 additions and 6 deletions

View File

@@ -12,13 +12,14 @@ import os
import tempfile
from datetime import datetime, timezone
from flask import current_app
from sqlalchemy import func
from shopdb.api import db, Application
from .models import (
ManifestScope, ManifestPublishedVersion, ManifestEnforcementReport,
ManifestEnforcementResult, ManifestPayload,
ManifestEnforcementResult, ManifestPayload, ManifestBlob,
)
from .importer import build_entry
from .serializer import scope_to_json
@@ -276,6 +277,43 @@ def compliance_for_scope(scope):
}
def _payload_dir():
d = os.path.join(current_app.instance_path, 'geenforce', 'payloads')
os.makedirs(d, exist_ok=True)
return d
def blob_path(sha256):
"""Filesystem path where a blob's bytes live (may or may not exist)."""
return os.path.join(_payload_dir(), sha256)
def store_blob(rawbytes, filename, contenttype=None):
"""Store bytes in the content-addressed payload store; return the sha256.
Deduped by content hash - an already-present blob is not rewritten. Upserts
a ManifestBlob registry row (uncommitted). Entries reference it by
payloadsha256; the client fetches GET /api/geenforce/payload/<sha256>.
"""
sha = hashlib.sha256(rawbytes).hexdigest()
path = blob_path(sha)
if not os.path.exists(path):
fd, tmp = tempfile.mkstemp(dir=_payload_dir(), suffix='.tmp')
try:
with os.fdopen(fd, 'wb') as handle:
handle.write(rawbytes)
os.replace(tmp, path)
except Exception:
if os.path.exists(tmp):
os.remove(tmp)
raise
if not db.session.get(ManifestBlob, sha):
db.session.add(ManifestBlob(
sha256=sha, filename=filename, contenttype=contenttype,
sizebytes=len(rawbytes), createdat=_utcnow()))
return sha
def store_inline_payload(entry, filename, contenttype, rawbytes):
"""Replace the entry's inline payload with these bytes (uncommitted).