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

@@ -8,9 +8,10 @@ Two audiences:
collector's managed-token pattern (X-API-Key or Bearer PAT).
"""
import os
from functools import wraps
from flask import Blueprint, request, Response
from flask import Blueprint, request, Response, send_file
from flask_jwt_extended import jwt_required
from sqlalchemy.exc import IntegrityError
@@ -23,7 +24,7 @@ SHAREROOT_SETTING = 'geenforce_share_root'
from ..models import (
ManifestScope, ManifestEntry, ManifestPublishedVersion,
ManifestEnforcementReport, ManifestPayload, ENTRY_TYPES, PHASES,
ManifestEnforcementReport, ManifestPayload, ManifestBlob, ENTRY_TYPES, PHASES,
)
from ..serializer import scope_to_manifest, entry_to_dict
from ..importer import build_entry, populate_entry
@@ -98,6 +99,48 @@ def get_manifest():
'X-Manifest-Version': str(published.versionnumber)})
# -- client payload download (share-less installer delivery) ------------------
@geenforce_bp.route('/payload/<sha256>', methods=['GET'])
@require_fetch_token
def get_payload(sha256):
"""Serve a payload blob by content hash over HTTPS.
Lets share-less (Intune/local-account) PCs pull installers the manifest
references without SMB. Sources: the content-addressed blob store (large
http payloads) first, then an inline DB payload with this hash. The client
re-verifies the sha256, so the hash IS the integrity guarantee. ETag = the
hash (content is immutable).
"""
sha = (sha256 or '').strip().lower()
if len(sha) != 64 or any(c not in '0123456789abcdef' for c in sha):
return error_response(ErrorCodes.VALIDATION_ERROR, 'bad sha256',
http_code=400)
etag = f'"{sha}"'
if request.headers.get('If-None-Match') == etag:
return Response(status=304, headers={'ETag': etag})
blob = db.session.get(ManifestBlob, sha)
if blob and os.path.isfile(service.blob_path(sha)):
response = send_file(
service.blob_path(sha),
mimetype=blob.contenttype or 'application/octet-stream',
as_attachment=True, download_name=blob.filename)
response.headers['ETag'] = etag
return response
inline = ManifestPayload.query.filter_by(payloadsha256=sha).first()
if inline:
return Response(
inline.payloadbytes,
mimetype=inline.contenttype or 'application/octet-stream',
headers={'ETag': etag,
'Content-Disposition': f'attachment; filename="{inline.filename}"'})
return error_response(ErrorCodes.NOT_FOUND, 'No payload for that hash',
http_code=404)
# -- client reporting (observed state) ----------------------------------------
@geenforce_bp.route('/report', methods=['POST'])