mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-03 10:36:45 +09:00
Comments, docstrings and a few strings cited plan numbers ("plan 29
#9", "plan 86 §B2") from a git-ignored folder, so they pointed at
nothing; one reached users in a bottleneck hint. Code and frontend
strings are unchanged apart from those citations (checked by AST and
token comparison). A vhost still carrying the old asset comment reads
as undrifted, and a guard test keeps new citations out.
265 lines
11 KiB
Python
265 lines
11 KiB
Python
"""The managed profile: ServerKit Cloud's signed presentation policy.
|
|
|
|
When a customer's Managed scope says ServerKit Cloud runs the server for them,
|
|
Cloud sends one ``ui.managed_profile`` command over the same signed path as
|
|
every other command. What it carries is a *presentation* policy: which
|
|
capabilities Cloud has taken over, who holds them, and when the policy lapses.
|
|
This module is the only writer of ``managed-profile.json`` — kept next to
|
|
``connect.json`` because it belongs to the pairing and leaves with it — and
|
|
the only place that decides whether the profile currently applies.
|
|
|
|
The rules that keep this truthful:
|
|
|
|
- **Hidden, never removed.** The policy changes what the panel *shows*. Every
|
|
route stays registered and every API keeps answering with the caller's
|
|
normal role; support, break-glass, exports and Cloud's own handoff to
|
|
``/fleet`` all keep landing.
|
|
- **Expiry lifts the profile.** A panel that cannot reach Cloud past
|
|
``expires_at`` shows everything again — a managed customer whose provider
|
|
disappears must be able to run their own server. An expired document is
|
|
kept (the UI says the profile lapsed) but holds nothing.
|
|
- **Revoked or unpaired clears it.** A panel that is no longer connected is
|
|
not managed; ``clear()`` runs on local disconnect and on relay revocation.
|
|
- **An override lifts it without deleting it.** A support session sees the
|
|
full panel until the override's end time, and the profile returns on its
|
|
own afterwards.
|
|
|
|
Every apply and lift is written to the panel's audit log with the policy id.
|
|
"""
|
|
import json
|
|
import logging
|
|
import os
|
|
from datetime import datetime, timezone
|
|
|
|
from app import paths
|
|
from app.services.connect_commands import handler
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
PROFILE_FILENAME = 'managed-profile.json'
|
|
|
|
# The launch capability set. Cloud validates against its
|
|
# own copy before signing; the panel stores what it received and the UI maps
|
|
# the ids it knows, so an id from a newer Cloud simply maps to nothing here.
|
|
CAPABILITIES = (
|
|
'fleet', # Servers, Agent Fleet, Fleet Proxy
|
|
'provisioning', # Cloud Servers
|
|
'config_templates', # Server Templates
|
|
'updates', # panel/agent update screens and self-update
|
|
'backups_schedule', # backup scheduling and destinations; restore stays
|
|
'monitoring_alerts', # alert rules and channels; live charts stay
|
|
'firewall',
|
|
)
|
|
|
|
ACTION_APPLIED = 'connect.managed_profile_applied'
|
|
ACTION_CLEARED = 'connect.managed_profile_cleared'
|
|
|
|
|
|
def profile_path() -> str:
|
|
return os.path.join(paths.SERVERKIT_CONFIG_DIR, PROFILE_FILENAME)
|
|
|
|
|
|
def _read() -> dict:
|
|
path = profile_path()
|
|
if not os.path.exists(path):
|
|
return {}
|
|
try:
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
return data if isinstance(data, dict) else {}
|
|
except (OSError, ValueError) as exc:
|
|
logger.warning('Could not read %s: %s', path, exc)
|
|
return {}
|
|
|
|
|
|
def _write(payload: dict):
|
|
path = profile_path()
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
with open(path, 'w', encoding='utf-8') as f:
|
|
json.dump(payload, f, indent=2)
|
|
f.write('\n')
|
|
if os.name != 'nt':
|
|
os.chmod(path, 0o600)
|
|
|
|
|
|
def _parse_dt(value):
|
|
if not value:
|
|
return None
|
|
try:
|
|
dt = datetime.fromisoformat(str(value).replace('Z', '+00:00'))
|
|
except ValueError:
|
|
return None
|
|
if dt.tzinfo is None:
|
|
dt = dt.replace(tzinfo=timezone.utc)
|
|
return dt
|
|
|
|
|
|
def _audit(action: str, details: dict, app=None):
|
|
"""Best effort and never in the policy's way: a profile that applied but
|
|
could not be audited still applies, and the warning says why."""
|
|
try:
|
|
if app is None:
|
|
from flask import has_app_context
|
|
if not has_app_context():
|
|
return
|
|
from app.services.audit_service import AuditService
|
|
def go():
|
|
AuditService.log(action, target_type='connect',
|
|
target_id=details.get('policy_id'), details=details)
|
|
if app is not None:
|
|
with app.app_context():
|
|
go()
|
|
else:
|
|
go()
|
|
except Exception:
|
|
logger.warning('Managed profile: could not write the audit entry', exc_info=True)
|
|
|
|
|
|
def clear(reason: str, app=None, policy_id: str = None) -> bool:
|
|
"""Remove the profile document. Returns True when one was there to lift.
|
|
|
|
Every lift is audited with the policy id that was in force and the reason
|
|
('cleared' from Cloud, 'revoked', 'disconnected')."""
|
|
doc = _read()
|
|
path = profile_path()
|
|
if os.path.exists(path):
|
|
try:
|
|
os.unlink(path)
|
|
except OSError as exc:
|
|
logger.warning('Could not remove %s: %s', path, exc)
|
|
return False
|
|
if doc:
|
|
_audit(ACTION_CLEARED, {
|
|
'policy_id': doc.get('policy_id') or policy_id,
|
|
'reason': reason,
|
|
'capabilities': doc.get('capabilities') or [],
|
|
}, app=app)
|
|
return bool(doc)
|
|
|
|
|
|
def current(now: datetime = None) -> dict:
|
|
"""The effective profile for the API and the UI.
|
|
|
|
``active`` is the single question the sidebar, pages and in-page controls
|
|
ask. An expired policy or a live override both answer False — the full
|
|
panel shows — while the document's detail still comes back so Settings
|
|
can say exactly what lapsed or who lifted it and until when.
|
|
"""
|
|
doc = _read()
|
|
if not doc:
|
|
return {
|
|
'profile': 'none',
|
|
'active': False,
|
|
'capabilities': [],
|
|
'held_by': None,
|
|
'since': None,
|
|
'expires_at': None,
|
|
'support_url': None,
|
|
'policy_id': None,
|
|
'applied_at': None,
|
|
'expired': False,
|
|
'override': None,
|
|
}
|
|
now = now or datetime.now(timezone.utc)
|
|
expires_at = _parse_dt(doc.get('expires_at'))
|
|
expired = expires_at is None or now >= expires_at
|
|
override = doc.get('override') if isinstance(doc.get('override'), dict) else None
|
|
override_until = _parse_dt((override or {}).get('until'))
|
|
override_active = override is not None and override_until is not None and now < override_until
|
|
capabilities = [c for c in (doc.get('capabilities') or []) if isinstance(c, str)]
|
|
active = bool(capabilities) and not expired and not override_active
|
|
return {
|
|
'profile': doc.get('profile') or 'managed',
|
|
'active': active,
|
|
# The UI only ever reads held capabilities when active is True; the
|
|
# full list stays in the document either way for Settings.
|
|
'capabilities': capabilities if active else [],
|
|
'held_capabilities': capabilities,
|
|
'held_by': doc.get('held_by'),
|
|
'since': doc.get('since'),
|
|
'expires_at': doc.get('expires_at'),
|
|
'support_url': doc.get('support_url'),
|
|
'policy_id': doc.get('policy_id'),
|
|
'applied_at': doc.get('applied_at'),
|
|
'expired': expired,
|
|
'override': {'until': override.get('until'), 'reason': override.get('reason')}
|
|
if override_active else None,
|
|
}
|
|
|
|
|
|
def _validate(args: dict) -> dict:
|
|
"""The document to store, or ValueError with the sentence Cloud shows."""
|
|
policy_id = str(args.get('policy_id') or '').strip()
|
|
if not policy_id:
|
|
raise ValueError('The policy carried no id, so it was not applied.')
|
|
profile = str(args.get('profile') or 'managed')
|
|
if profile == 'none':
|
|
return {'policy_id': policy_id, 'profile': 'none', 'capabilities': []}
|
|
if profile != 'managed':
|
|
raise ValueError(f'Unknown managed profile {profile!r}.')
|
|
capabilities = args.get('capabilities')
|
|
if not isinstance(capabilities, list) or not all(isinstance(c, str) for c in capabilities):
|
|
raise ValueError('The policy capabilities were not a list of names.')
|
|
expires_at = _parse_dt(args.get('expires_at'))
|
|
if expires_at is None:
|
|
# A policy without an end never lifts itself, which is exactly the
|
|
# failure the expiry rule exists to prevent. Refuse it rather than
|
|
# store it.
|
|
raise ValueError('The policy carried no expiry, so it was not applied.')
|
|
override = args.get('override')
|
|
if override is not None:
|
|
if not isinstance(override, dict) or _parse_dt(override.get('until')) is None:
|
|
raise ValueError('The policy override carried no end time, so it was not applied.')
|
|
override = {'until': override.get('until'),
|
|
'reason': str(override.get('reason') or '')[:500]}
|
|
return {
|
|
'policy_id': policy_id,
|
|
'profile': 'managed',
|
|
'capabilities': sorted({c.strip() for c in capabilities if c.strip()}),
|
|
'held_by': str(args.get('held_by') or 'ServerKit Cloud')[:200],
|
|
'since': args.get('since'),
|
|
'expires_at': expires_at.isoformat(),
|
|
'support_url': (str(args.get('support_url'))[:500] if args.get('support_url') else None),
|
|
'override': override,
|
|
}
|
|
|
|
|
|
@handler('ui.managed_profile')
|
|
def apply_profile(args: dict, app=None) -> dict:
|
|
"""Apply (or clear) the managed profile Cloud signed.
|
|
|
|
The result output is a small JSON document carrying the policy id back as
|
|
the acknowledgement; Cloud matches it against what it sent and re-sends
|
|
once when they disagree."""
|
|
try:
|
|
doc = _validate(args or {})
|
|
except ValueError as exc:
|
|
return {'ok': False, 'code': 400, 'summary': str(exc)}
|
|
|
|
if doc['profile'] == 'none' or not doc['capabilities']:
|
|
had = clear('cleared', app=app, policy_id=doc['policy_id'])
|
|
summary = ('The managed profile was lifted; the full panel is back.'
|
|
if had else 'No managed profile was in force; nothing changed.')
|
|
return {'ok': True, 'summary': summary,
|
|
'output': json.dumps({'policy_id': doc['policy_id'], 'applied': False})}
|
|
|
|
doc['applied_at'] = datetime.now(timezone.utc).isoformat()
|
|
try:
|
|
_write(doc)
|
|
except OSError as exc:
|
|
return {'ok': False, 'code': 500,
|
|
'summary': f'The profile could not be stored ({exc}), so nothing was applied.'}
|
|
_audit(ACTION_APPLIED, {
|
|
'policy_id': doc['policy_id'],
|
|
'capabilities': doc['capabilities'],
|
|
'held_by': doc['held_by'],
|
|
'since': doc['since'],
|
|
'expires_at': doc['expires_at'],
|
|
'override': doc['override'],
|
|
}, app=app)
|
|
return {'ok': True,
|
|
'summary': f"Managed profile applied: {len(doc['capabilities'])} "
|
|
f"capabilit{'y is' if len(doc['capabilities']) == 1 else 'ies are'} "
|
|
f"held by {doc['held_by']} until {doc['expires_at']}.",
|
|
'output': json.dumps({'policy_id': doc['policy_id'], 'applied': True})}
|