mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-28 07:37:56 +09:00
Only three routes checked can_access_server; the rest loaded the server by id, so a developer outside a workspace could run cron, write files, install packages or widen agent permissions on its servers, and the list/overview/docker picker showed every server. Every <server_id> route now resolves through the workspace check, lists filter in SQL, and PUT /servers/<id> validates allowed_ips like PUT /allowed-ips. A sweep test walks the URL map so a new unguarded route fails CI.
82 lines
2.8 KiB
Python
82 lines
2.8 KiB
Python
"""
|
|
Fleet API
|
|
|
|
Cross-server endpoints that don't belong to any single server's resource
|
|
tree — primarily the target-picker support that every agent-aware
|
|
feature page consumes.
|
|
|
|
The "target" abstraction:
|
|
|
|
target = {"kind": "local"}
|
|
| {"kind": "agent", "server_id": "...", "server_name": "..."}
|
|
|
|
Pages render a picker; clicks send `target` in the request body of
|
|
feature CRUD calls; the backend dispatches against `target.kind`. See
|
|
`backend/app/services/fleet_targeting.py` (Phase 1) for the dispatch
|
|
helper.
|
|
|
|
This module is read-only; it does not write any state.
|
|
"""
|
|
|
|
from flask import Blueprint, jsonify, request
|
|
from flask_jwt_extended import jwt_required
|
|
|
|
from app.services.agent_registry import agent_registry
|
|
from app.services.workspace_service import WorkspaceService
|
|
from app.middleware.rbac import get_current_user
|
|
|
|
|
|
fleet_bp = Blueprint('fleet', __name__)
|
|
|
|
|
|
@fleet_bp.route('/targets', methods=['GET'])
|
|
@jwt_required()
|
|
def list_targets():
|
|
"""List runnable targets, optionally filtered by feature.
|
|
|
|
Query params:
|
|
feature — capability name (cron, docker, systemd, …). When
|
|
present, only agents whose advertised capability
|
|
map has feature=true are returned. When absent,
|
|
every connected agent is returned regardless of
|
|
capabilities.
|
|
|
|
The local panel host is always included as the first entry. Even
|
|
if no agent has the requested feature the caller still gets the
|
|
local target back, which matches the behaviour the UI expects:
|
|
"the local target is always available, agents are additive."
|
|
"""
|
|
feature = (request.args.get('feature') or '').strip() or None
|
|
|
|
targets = [{'kind': 'local'}]
|
|
user = get_current_user()
|
|
|
|
# Walk the in-memory registry of connected agents. We intentionally
|
|
# do NOT pull from the Server table — capabilities are per-session
|
|
# state, and a server with status=offline can't take a command no
|
|
# matter what it last reported.
|
|
for server_id in agent_registry.get_connected_servers():
|
|
if feature and not agent_registry.has_capability(server_id, feature):
|
|
continue
|
|
|
|
# Look up the display name. We could cache this on
|
|
# ConnectedAgent, but the table is small and the lookup is
|
|
# cheap; not worth the staleness risk.
|
|
server = WorkspaceService.get_accessible_server(user, server_id)
|
|
if not server:
|
|
# Connected agent without a Server row would be a bug
|
|
# elsewhere; skip rather than 500.
|
|
continue
|
|
|
|
caps = agent_registry.get_capabilities(server_id) or {}
|
|
|
|
targets.append({
|
|
'kind': 'agent',
|
|
'server_id': server.id,
|
|
'server_name': server.name,
|
|
'hostname': server.hostname,
|
|
'capabilities': caps,
|
|
})
|
|
|
|
return jsonify({'targets': targets})
|