Add per-version staging, retention (keep 3), and upload/rollback routes for shopdbagent.exe and the tray exe. Keep upload under endpoints.agentpackage and declare a distinct endpoints.agentupdate permission for the future live-push pipe so app-deploy rights never imply SYSTEM agent replacement.
127 lines
5.0 KiB
Python
127 lines
5.0 KiB
Python
"""Agent binary upload and retention routes.
|
|
|
|
Lets an operator drop in a new shopdbagent.exe or shopdb-endpoint-tray.exe from
|
|
the Endpoints page, keep the last few versions, and roll the current pointer
|
|
back to a retained build. The actual staging/retention logic lives in
|
|
agentstore.py; this module is only the HTTP surface and authorization.
|
|
|
|
Authorization boundary (deliberate):
|
|
- Uploading, listing, and selecting the current staged version is
|
|
endpoints.agentpackage - the same grant that already lets someone build and
|
|
download the site-agent ZIP. Staging a binary only changes what a FUTURE
|
|
download contains; it does not touch any running endpoint.
|
|
- Pushing that binary to live endpoints (agent replacement) is a separate,
|
|
heavier grant, endpoints.agentupdate, and is NOT wired here. It is a distinct
|
|
command family from application deploys (endpoints.deploy) so app-push rights
|
|
never imply SYSTEM-level agent replacement. That push pipe is a later slice.
|
|
|
|
Uploads are capped and PE-checked so a wrong or hostile file cannot be staged.
|
|
"""
|
|
|
|
from flask import request
|
|
|
|
from shopdb.api import (success_response, error_response, ErrorCodes,
|
|
require_permission)
|
|
|
|
from .routes import endpoints_bp
|
|
from . import agentstore
|
|
|
|
# Hard cap on an uploaded agent binary. The real agent is a few MB; this is a
|
|
# generous ceiling that still refuses an operator who points at a giant wrong
|
|
# file before it is buffered fully into memory-staged storage.
|
|
_MAX_UPLOAD_BYTES = 64 * 1024 * 1024
|
|
|
|
|
|
@endpoints_bp.route('/siteagent/binaries/<binkey>', methods=['GET'])
|
|
@require_permission('endpoints.agentpackage')
|
|
def list_agent_binaries(binkey):
|
|
"""List staged versions for a binary (agent or tray), newest first."""
|
|
if not agentstore.is_valid_binary_key(binkey):
|
|
return error_response(
|
|
ErrorCodes.VALIDATION_ERROR,
|
|
'binkey must be one of: agent, tray',
|
|
http_code=400)
|
|
return success_response({
|
|
'binary': binkey,
|
|
'current': agentstore.current_version(binkey),
|
|
'versions': agentstore.list_versions(binkey),
|
|
})
|
|
|
|
|
|
@endpoints_bp.route('/siteagent/binaries/<binkey>', methods=['POST'])
|
|
@require_permission('endpoints.agentpackage')
|
|
def upload_agent_binary(binkey):
|
|
"""Stage an uploaded binary as a new version and make it current.
|
|
|
|
Expects multipart/form-data with a 'file' upload and a 'version' field. The
|
|
version must be a strict dotted-numeric string (for example 1.6.3) so it is
|
|
safe as a directory name and orderable. The upload must be a Windows PE
|
|
executable. Older versions past the retention window are pruned, but never
|
|
the current one.
|
|
"""
|
|
version = (request.form.get('version') or '').strip()
|
|
if not agentstore.is_valid_binary_key(binkey):
|
|
return error_response(
|
|
ErrorCodes.VALIDATION_ERROR,
|
|
'binkey must be one of: agent, tray',
|
|
http_code=400)
|
|
if not agentstore.is_valid_version(version):
|
|
return error_response(
|
|
ErrorCodes.VALIDATION_ERROR,
|
|
'version must be a dotted-numeric string such as 1.6.3',
|
|
http_code=400)
|
|
|
|
upload = request.files.get('file')
|
|
if upload is None or not upload.filename:
|
|
return error_response(
|
|
ErrorCodes.VALIDATION_ERROR,
|
|
'a file upload is required',
|
|
http_code=400)
|
|
|
|
data = upload.read(_MAX_UPLOAD_BYTES + 1)
|
|
if len(data) > _MAX_UPLOAD_BYTES:
|
|
return error_response(
|
|
ErrorCodes.VALIDATION_ERROR,
|
|
'uploaded binary exceeds the size limit',
|
|
http_code=400)
|
|
if not agentstore.looks_like_pe(data):
|
|
return error_response(
|
|
ErrorCodes.VALIDATION_ERROR,
|
|
'uploaded file is not a Windows executable',
|
|
http_code=400)
|
|
|
|
result = agentstore.store_version(binkey, version, data)
|
|
return success_response({
|
|
'binary': binkey,
|
|
'current': result['current'],
|
|
'versions': result['versions'],
|
|
})
|
|
|
|
|
|
@endpoints_bp.route('/siteagent/binaries/<binkey>/current', methods=['PUT'])
|
|
@require_permission('endpoints.agentpackage')
|
|
def set_current_agent_binary(binkey):
|
|
"""Point 'current' at an already-staged version (roll back or forward)."""
|
|
if not agentstore.is_valid_binary_key(binkey):
|
|
return error_response(
|
|
ErrorCodes.VALIDATION_ERROR,
|
|
'binkey must be one of: agent, tray',
|
|
http_code=400)
|
|
source = request.get_json(silent=True) or {}
|
|
version = (source.get('version') or '').strip()
|
|
if not agentstore.is_valid_version(version):
|
|
return error_response(
|
|
ErrorCodes.VALIDATION_ERROR,
|
|
'version must be a dotted-numeric string such as 1.6.3',
|
|
http_code=400)
|
|
if not agentstore.set_current(binkey, version):
|
|
return error_response(
|
|
ErrorCodes.NOT_FOUND,
|
|
'that version is not staged',
|
|
http_code=404)
|
|
return success_response({
|
|
'binary': binkey,
|
|
'current': agentstore.current_version(binkey),
|
|
'versions': agentstore.list_versions(binkey),
|
|
})
|