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:
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user