mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 05:24:49 +09:00
Four foundational enhancements toward the deploy-code developer experience: - Container status aggregator (C7): new ContainerStatusService applies a deterministic priority hierarchy (degraded > restarting > running:unhealthy > starting > running:healthy > exited > unknown) over an app's containers, cached with a short TTL. Exposed at /api/v1/status and pushed over a new container_status Socket.IO channel; Applications page now renders an aggregated ContainerStatusPill with sub-status tooltip. - API token scopes + sensitive data masking (C10): require_scope() decorator enforces fine-grained scopes for API-key requests (pass-through for JWT), a canonical SCOPES catalog at /api/v1/api-keys/scopes, and a reusable sensitive_data_filter (mask_sensitive) honoring secrets:read. Frontend gains a grouped ApiKeyScopesModal picker and per-key scope pills. - Server onboarding state machine (C8): new onboarding lifecycle (pending -> validating -> installing_prerequisites -> installing_docker -> pairing_agent -> ready/failed) with ServerOnboardingService, a server_onboarding_logs table, servers.onboarding_* columns, REST endpoints, and an OnboardingWizard surfaced on the server detail overview. - Declarative template catalog (C5): documented catalog schema and magic-variable resolution (SERVICE_PASSWORD_/USER_/FQDN_/URL_/BASE64_*) layered onto the existing YAML template loader without changing behavior for templates that don't use them; GET /api/v1/templates/catalog/schema. Migration 029 adds the onboarding table + columns. 60 new tests pass; frontend builds clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
118 lines
5.0 KiB
Python
118 lines
5.0 KiB
Python
"""API key scope enforcement middleware.
|
|
|
|
This complements RBAC: RBAC governs *who* the user is (role/permissions),
|
|
whereas API-key scopes govern *what a given key is allowed to do*. The two are
|
|
orthogonal — a key is always owned by a user, and the user's RBAC still applies
|
|
on top of the key's scopes.
|
|
|
|
The ``require_scope`` decorator is a **pass-through for JWT/session requests**:
|
|
only requests authenticated with an ``X-API-Key`` header (i.e. ``g.api_key`` is
|
|
set by :mod:`app.middleware.api_key_auth`) are scope-checked. JWT users are
|
|
governed by RBAC decorators instead. This lets the same endpoint be safely
|
|
shared between the web UI (JWT) and programmatic clients (API keys).
|
|
"""
|
|
from functools import wraps
|
|
|
|
from flask import g, jsonify
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Canonical scope catalog
|
|
# ---------------------------------------------------------------------------
|
|
# Each entry: {key, label, group, description}. ``'*'`` (full access) is handled
|
|
# implicitly by ApiKey.has_scope() and surfaced in the UI as a master toggle, so
|
|
# it is not listed as a row here — see FULL_ACCESS_SCOPE.
|
|
FULL_ACCESS_SCOPE = '*'
|
|
|
|
SCOPES = [
|
|
# Coarse-grained
|
|
{'key': 'read', 'label': 'Read (all)', 'group': 'General',
|
|
'description': 'Read-only access across all resources.'},
|
|
{'key': 'write', 'label': 'Write (all)', 'group': 'General',
|
|
'description': 'Create and modify access across all resources.'},
|
|
|
|
# Applications
|
|
{'key': 'apps:read', 'label': 'View applications', 'group': 'Applications',
|
|
'description': 'List and inspect managed applications.'},
|
|
{'key': 'apps:write', 'label': 'Manage applications', 'group': 'Applications',
|
|
'description': 'Create, update, start/stop, and delete applications.'},
|
|
{'key': 'apps:deploy', 'label': 'Deploy applications', 'group': 'Applications',
|
|
'description': 'Trigger builds and deployments.'},
|
|
|
|
# Databases
|
|
{'key': 'databases:read', 'label': 'View databases', 'group': 'Databases',
|
|
'description': 'List databases and inspect schemas.'},
|
|
{'key': 'databases:write', 'label': 'Manage databases', 'group': 'Databases',
|
|
'description': 'Create, alter, and drop databases and users.'},
|
|
|
|
# Domains
|
|
{'key': 'domains:read', 'label': 'View domains', 'group': 'Domains',
|
|
'description': 'List domains and SSL status.'},
|
|
{'key': 'domains:write', 'label': 'Manage domains', 'group': 'Domains',
|
|
'description': 'Attach domains and manage certificates.'},
|
|
|
|
# DNS
|
|
{'key': 'dns:read', 'label': 'View DNS records', 'group': 'DNS',
|
|
'description': 'List DNS zones and records.'},
|
|
{'key': 'dns:write', 'label': 'Manage DNS records', 'group': 'DNS',
|
|
'description': 'Create, update, and delete DNS records.'},
|
|
|
|
# Backups
|
|
{'key': 'backups:read', 'label': 'View backups', 'group': 'Backups',
|
|
'description': 'List backup policies and runs.'},
|
|
{'key': 'backups:write', 'label': 'Manage backups', 'group': 'Backups',
|
|
'description': 'Create policies, trigger backups, and restore.'},
|
|
|
|
# Servers
|
|
{'key': 'servers:read', 'label': 'View servers', 'group': 'Servers',
|
|
'description': 'List servers, agents, and metrics.'},
|
|
{'key': 'servers:admin', 'label': 'Administer servers', 'group': 'Servers',
|
|
'description': 'Run privileged server and agent operations.'},
|
|
|
|
# Secrets
|
|
{'key': 'secrets:read', 'label': 'Reveal secrets', 'group': 'Secrets',
|
|
'description': 'Read unmasked secret values in API responses.'},
|
|
]
|
|
|
|
# Set of valid catalog keys (excludes the implicit full-access scope).
|
|
SCOPE_KEYS = {entry['key'] for entry in SCOPES}
|
|
|
|
|
|
def require_scope(*scopes):
|
|
"""Enforce that an API-key request carries ALL of ``scopes``.
|
|
|
|
Semantics:
|
|
* If the request is **API-key authenticated** (``g.api_key`` is set), the
|
|
key must satisfy every listed scope via ``ApiKey.has_scope()`` (which
|
|
honors ``'*'`` full access and ``'resource:*'`` wildcards). The first
|
|
unsatisfied scope yields ``403``.
|
|
* If the request is **not** API-key authenticated (JWT/session user), the
|
|
decorator is a pass-through — those users are governed by RBAC.
|
|
|
|
This decorator does not itself require authentication; compose it with an
|
|
auth decorator (``@jwt_required()``, ``@auth_required()``, etc.) when a route
|
|
needs to be authenticated.
|
|
|
|
Usage::
|
|
|
|
@bp.route('/things')
|
|
@auth_required()
|
|
@require_scope('apps:read')
|
|
def list_things():
|
|
...
|
|
"""
|
|
def decorator(fn):
|
|
@wraps(fn)
|
|
def wrapper(*args, **kwargs):
|
|
api_key = getattr(g, 'api_key', None)
|
|
if api_key is not None:
|
|
for scope in scopes:
|
|
if not api_key.has_scope(scope):
|
|
return jsonify({
|
|
'error': f'Insufficient API key scope: {scope}'
|
|
}), 403
|
|
# JWT/session requests pass through (governed by RBAC).
|
|
return fn(*args, **kwargs)
|
|
return wrapper
|
|
return decorator
|