mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-14 21:15:47 -07:00
Locked-down endpoints: /api/agents, /api/devices, /api/devices/<guid>, /api/device/details/<hostname>, /api/device/description/<hostname>, /api/device_list_views, /api/device_list_views/<view_id>, /api/sites, /api/sites/delete, /api/sites/device_map, /api/sites/assign, /api/sites/rename, /api/repo/current_hash, /api/agent/hash_list, /api/scripts/quick_run, /api/ansible/quick_run, /api/device/activity/<hostname>, /api/device/activity/job/<job_id>, /api/server/time.
This commit is contained in:
@@ -28,6 +28,7 @@ if TYPE_CHECKING: # pragma: no cover - typing aide
|
||||
from .. import EngineServiceAdapters
|
||||
|
||||
from ...assemblies.service import AssemblyRuntimeService
|
||||
from ...auth import RequestAuthContext
|
||||
|
||||
|
||||
def _assemblies_root() -> Path:
|
||||
@@ -404,9 +405,18 @@ def register_execution(app: "Flask", adapters: "EngineServiceAdapters") -> None:
|
||||
if assembly_cache is None:
|
||||
raise RuntimeError("Assembly cache is not initialised; ensure Engine bootstrap executed.")
|
||||
assembly_runtime = AssemblyRuntimeService(assembly_cache, logger=adapters.context.logger)
|
||||
auth = RequestAuthContext(
|
||||
app=app,
|
||||
dev_mode_manager=adapters.dev_mode_manager,
|
||||
config=adapters.config,
|
||||
logger=adapters.context.logger,
|
||||
)
|
||||
|
||||
@blueprint.route("/api/scripts/quick_run", methods=["POST"])
|
||||
def scripts_quick_run():
|
||||
user, error = auth.require_user()
|
||||
if error:
|
||||
return jsonify(error[0]), error[1]
|
||||
data = request.get_json(silent=True) or {}
|
||||
rel_path_input = data.get("script_path")
|
||||
rel_path_normalized = _normalize_script_relpath(rel_path_input)
|
||||
@@ -419,6 +429,7 @@ def register_execution(app: "Flask", adapters: "EngineServiceAdapters") -> None:
|
||||
return jsonify({"error": "Missing script_path or hostnames[]"}), 400
|
||||
|
||||
rel_path_canonical = rel_path_normalized
|
||||
username = (user.get("username") if isinstance(user, dict) else None) or "unknown"
|
||||
|
||||
assembly_source = "runtime"
|
||||
assembly_guid: Optional[str] = None
|
||||
@@ -455,7 +466,7 @@ def register_execution(app: "Flask", adapters: "EngineServiceAdapters") -> None:
|
||||
except Exception as exc: # pragma: no cover - defensive guard
|
||||
service_log(
|
||||
"assemblies",
|
||||
f"quick job failed to resolve script path={rel_path_input!r}: {exc}",
|
||||
f"quick job failed to resolve script path={rel_path_input!r} user={username}: {exc}",
|
||||
level="ERROR",
|
||||
)
|
||||
return jsonify({"error": "Failed to resolve script path"}), 500
|
||||
@@ -470,7 +481,7 @@ def register_execution(app: "Flask", adapters: "EngineServiceAdapters") -> None:
|
||||
if not within_scripts or not os.path.isfile(abs_path_str):
|
||||
service_log(
|
||||
"assemblies",
|
||||
f"quick job requested missing or out-of-scope script input={rel_path_input!r} normalized={rel_path_canonical}",
|
||||
f"quick job requested missing or out-of-scope script input={rel_path_input!r} normalized={rel_path_canonical} user={username}",
|
||||
level="WARNING",
|
||||
)
|
||||
return jsonify({"error": "Script not found"}), 404
|
||||
@@ -597,7 +608,7 @@ def register_execution(app: "Flask", adapters: "EngineServiceAdapters") -> None:
|
||||
results.append({"hostname": host, "job_id": job_id, "status": "Running"})
|
||||
service_log(
|
||||
"assemblies",
|
||||
f"quick job queued hostname={host} path={rel_path_canonical} run_mode={run_mode} source={assembly_source}",
|
||||
f"quick job queued hostname={host} path={rel_path_canonical} run_mode={run_mode} source={assembly_source} requested_by={username}",
|
||||
)
|
||||
except Exception as exc:
|
||||
if conn is not None:
|
||||
@@ -611,10 +622,16 @@ def register_execution(app: "Flask", adapters: "EngineServiceAdapters") -> None:
|
||||
|
||||
@blueprint.route("/api/ansible/quick_run", methods=["POST"])
|
||||
def ansible_quick_run():
|
||||
_, error = auth.require_user()
|
||||
if error:
|
||||
return jsonify(error[0]), error[1]
|
||||
return jsonify({"error": "Ansible quick run is not yet available in the Engine runtime."}), 501
|
||||
|
||||
@blueprint.route("/api/device/activity/<hostname>", methods=["GET", "DELETE"])
|
||||
def device_activity(hostname: str):
|
||||
_, error = auth.require_user()
|
||||
if error:
|
||||
return jsonify(error[0]), error[1]
|
||||
conn = None
|
||||
try:
|
||||
conn = adapters.db_conn_factory()
|
||||
@@ -657,6 +674,9 @@ def register_execution(app: "Flask", adapters: "EngineServiceAdapters") -> None:
|
||||
|
||||
@blueprint.route("/api/device/activity/job/<int:job_id>", methods=["GET"])
|
||||
def device_activity_job(job_id: int):
|
||||
_, error = auth.require_user()
|
||||
if error:
|
||||
return jsonify(error[0]), error[1]
|
||||
conn = None
|
||||
try:
|
||||
conn = adapters.db_conn_factory()
|
||||
|
||||
@@ -4,24 +4,25 @@
|
||||
#
|
||||
# API Endpoints (if applicable):
|
||||
# - POST /api/agent/details (Device Authenticated) - Ingests hardware and inventory payloads from enrolled agents.
|
||||
# - GET /api/devices (No Authentication) - Returns a summary list of known devices for the WebUI transition.
|
||||
# - GET /api/devices/<guid> (No Authentication) - Retrieves a single device record by GUID, including summary fields.
|
||||
# - GET /api/device/details/<hostname> (No Authentication) - Returns full device details keyed by hostname.
|
||||
# - GET /api/agents (Token Authenticated) - Lists online collectors grouped by hostname and run context.
|
||||
# - GET /api/devices (Token Authenticated) - Returns a summary list of known devices for the WebUI transition.
|
||||
# - GET /api/devices/<guid> (Token Authenticated) - Retrieves a single device record by GUID, including summary fields.
|
||||
# - GET /api/device/details/<hostname> (Token Authenticated) - Returns full device details keyed by hostname.
|
||||
# - POST /api/device/description/<hostname> (Token Authenticated) - Updates the human-readable description for a device.
|
||||
# - GET /api/device_list_views (No Authentication) - Lists saved device table view definitions.
|
||||
# - GET /api/device_list_views/<int:view_id> (No Authentication) - Retrieves a specific saved device table view definition.
|
||||
# - GET /api/device_list_views (Token Authenticated) - Lists saved device table view definitions.
|
||||
# - GET /api/device_list_views/<int:view_id> (Token Authenticated) - Retrieves a specific saved device table view definition.
|
||||
# - POST /api/device_list_views (Token Authenticated) - Creates a custom device list view for the signed-in operator.
|
||||
# - PUT /api/device_list_views/<int:view_id> (Token Authenticated) - Updates an existing device list view definition.
|
||||
# - DELETE /api/device_list_views/<int:view_id> (Token Authenticated) - Deletes a saved device list view.
|
||||
# - GET /api/sites (No Authentication) - Lists known sites and their summary metadata.
|
||||
# - GET /api/sites (Token Authenticated) - Lists known sites and their summary metadata.
|
||||
# - POST /api/sites (Token Authenticated (Admin)) - Creates a new site for grouping devices.
|
||||
# - POST /api/sites/delete (Token Authenticated (Admin)) - Deletes one or more sites by identifier.
|
||||
# - GET /api/sites/device_map (No Authentication) - Provides hostname to site assignment mapping data.
|
||||
# - GET /api/sites/device_map (Token Authenticated) - Provides hostname to site assignment mapping data.
|
||||
# - POST /api/sites/assign (Token Authenticated (Admin)) - Assigns a set of devices to a given site.
|
||||
# - POST /api/sites/rename (Token Authenticated (Admin)) - Renames an existing site record.
|
||||
# - GET /api/repo/current_hash (No Authentication) - Fetches the current agent repository hash (with caching).
|
||||
# - GET /api/repo/current_hash (Token Authenticated) - Fetches the current agent repository hash (with caching).
|
||||
# - GET/POST /api/agent/hash (Device Authenticated) - Retrieves or updates an agent hash record bound to the authenticated device.
|
||||
# - GET /api/agent/hash_list (Loopback Restricted) - Returns stored agent hash metadata for localhost diagnostics.
|
||||
# - GET /api/agent/hash_list (Token Authenticated (Admin + Loopback)) - Returns stored agent hash metadata for localhost diagnostics.
|
||||
# ======================================================
|
||||
|
||||
"""Device management endpoints for the Borealis Engine API."""
|
||||
@@ -1584,21 +1585,37 @@ def register_management(app, adapters: "EngineServiceAdapters") -> None:
|
||||
|
||||
@blueprint.route("/api/agents", methods=["GET"])
|
||||
def _list_agents():
|
||||
requirement = service._require_login()
|
||||
if requirement:
|
||||
payload, status = requirement
|
||||
return jsonify(payload), status
|
||||
payload, status = service.list_agents()
|
||||
return jsonify(payload), status
|
||||
|
||||
@blueprint.route("/api/devices", methods=["GET"])
|
||||
def _list_devices():
|
||||
requirement = service._require_login()
|
||||
if requirement:
|
||||
payload, status = requirement
|
||||
return jsonify(payload), status
|
||||
payload, status = service.list_devices()
|
||||
return jsonify(payload), status
|
||||
|
||||
@blueprint.route("/api/devices/<guid>", methods=["GET"])
|
||||
def _device_by_guid(guid: str):
|
||||
requirement = service._require_login()
|
||||
if requirement:
|
||||
payload, status = requirement
|
||||
return jsonify(payload), status
|
||||
payload, status = service.get_device_by_guid(guid)
|
||||
return jsonify(payload), status
|
||||
|
||||
@blueprint.route("/api/device/details/<hostname>", methods=["GET"])
|
||||
def _device_details(hostname: str):
|
||||
requirement = service._require_login()
|
||||
if requirement:
|
||||
payload, status = requirement
|
||||
return jsonify(payload), status
|
||||
payload, status = service.get_device_details(hostname)
|
||||
return jsonify(payload), status
|
||||
|
||||
@@ -1615,11 +1632,19 @@ def register_management(app, adapters: "EngineServiceAdapters") -> None:
|
||||
|
||||
@blueprint.route("/api/device_list_views", methods=["GET"])
|
||||
def _list_views():
|
||||
requirement = service._require_login()
|
||||
if requirement:
|
||||
payload, status = requirement
|
||||
return jsonify(payload), status
|
||||
payload, status = service.list_views()
|
||||
return jsonify(payload), status
|
||||
|
||||
@blueprint.route("/api/device_list_views/<int:view_id>", methods=["GET"])
|
||||
def _get_view(view_id: int):
|
||||
requirement = service._require_login()
|
||||
if requirement:
|
||||
payload, status = requirement
|
||||
return jsonify(payload), status
|
||||
payload, status = service.get_view(view_id)
|
||||
return jsonify(payload), status
|
||||
|
||||
@@ -1679,6 +1704,10 @@ def register_management(app, adapters: "EngineServiceAdapters") -> None:
|
||||
|
||||
@blueprint.route("/api/sites", methods=["GET"])
|
||||
def _sites_list():
|
||||
requirement = service._require_login()
|
||||
if requirement:
|
||||
payload, status = requirement
|
||||
return jsonify(payload), status
|
||||
payload, status = service.list_sites()
|
||||
return jsonify(payload), status
|
||||
|
||||
@@ -1707,6 +1736,10 @@ def register_management(app, adapters: "EngineServiceAdapters") -> None:
|
||||
|
||||
@blueprint.route("/api/sites/device_map", methods=["GET"])
|
||||
def _sites_device_map():
|
||||
requirement = service._require_login()
|
||||
if requirement:
|
||||
payload, status = requirement
|
||||
return jsonify(payload), status
|
||||
payload, status = service.sites_device_map(request.args.get("hostnames"))
|
||||
return jsonify(payload), status
|
||||
|
||||
@@ -1732,11 +1765,19 @@ def register_management(app, adapters: "EngineServiceAdapters") -> None:
|
||||
|
||||
@blueprint.route("/api/repo/current_hash", methods=["GET"])
|
||||
def _repo_current_hash():
|
||||
requirement = service._require_login()
|
||||
if requirement:
|
||||
payload, status = requirement
|
||||
return jsonify(payload), status
|
||||
payload, status = service.repo_current_hash()
|
||||
return jsonify(payload), status
|
||||
|
||||
@blueprint.route("/api/agent/hash_list", methods=["GET"])
|
||||
def _agent_hash_list():
|
||||
requirement = service._require_admin()
|
||||
if requirement:
|
||||
payload, status = requirement
|
||||
return jsonify(payload), status
|
||||
payload, status = service.agent_hash_list()
|
||||
return jsonify(payload), status
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ from typing import TYPE_CHECKING, Any, Dict
|
||||
|
||||
from flask import Blueprint, Flask, jsonify
|
||||
|
||||
from ...auth import RequestAuthContext
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover - typing aide
|
||||
from .. import EngineServiceAdapters
|
||||
|
||||
@@ -31,13 +33,22 @@ def _serialize_time(now_local: datetime, now_utc: datetime) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def register_info(app: Flask, _adapters: "EngineServiceAdapters") -> None:
|
||||
def register_info(app: Flask, adapters: "EngineServiceAdapters") -> None:
|
||||
"""Expose server telemetry endpoints used by the admin interface."""
|
||||
|
||||
blueprint = Blueprint("engine_server_info", __name__)
|
||||
auth = RequestAuthContext(
|
||||
app=app,
|
||||
dev_mode_manager=adapters.dev_mode_manager,
|
||||
config=adapters.config,
|
||||
logger=adapters.context.logger,
|
||||
)
|
||||
|
||||
@blueprint.route("/api/server/time", methods=["GET"])
|
||||
def server_time() -> Any:
|
||||
_, error = auth.require_user()
|
||||
if error:
|
||||
return jsonify(error[0]), error[1]
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
now_local = now_utc.astimezone()
|
||||
payload = _serialize_time(now_local, now_utc)
|
||||
|
||||
@@ -111,6 +111,7 @@ The process that agents go through when authenticating securely with a Borealis
|
||||
- All device APIs now require Authorization: Bearer headers and a service-context (e.g. SYSTEM or CURRENTUSER) marker; missing, expired, mismatched, or revoked credentials are rejected before any business logic runs. Operator-driven revoking / device quarantining logic is not yet implemented.
|
||||
- Replay and credential theft defenses layer in DPoP proof validation (thumbprint binding) on the server side and short-lived access tokens (15 min) with 30-day refresh tokens hashed via SHA-256.
|
||||
- Centralized logging under Logs/Server and Agent/Logs captures enrollment approvals, rate-limit hits, signature failures, and auth anomalies for post-incident review.
|
||||
- The Engine’s operator-facing API endpoints (device inventory, assemblies, job history, etc.) require an authenticated operator session or bearer token; unauthenticated requests are rejected with 401/403 responses before any inventory or script metadata is returned and the requesting user is logged with each quick-run dispatch.
|
||||
#### Server Security
|
||||
- Auto-manages PKI: a persistent Borealis root CA (ECDSA SECP384R1) signs leaf certificates that include localhost SANs, tightened filesystem permissions, and a combined bundle for agent identity / cert pinning.
|
||||
- Script delivery is code-signed with an Ed25519 key stored under Certificates/Server/Code-Signing; agents refuse any payload whose signature or hash does not match the pinned public key.
|
||||
|
||||
Reference in New Issue
Block a user