mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-03 10:36:45 +09:00
GHSA-r4q7-x795-vr4j was one forgotten check, but the audit behind it found the same shape across the surface: `@jwt_required()` authenticates, it does not authorize, and ServerKit has no default-deny to catch the difference. This closes the routes that audit left open — fleet-monitor alert acknowledge/resolve, recycle-bin restore, nginx test/diff, registrar sync/test, server-template drift checks, uptime tracking on/off, server ping and remote compose ps/logs, refresh-capabilities, template test-db-connection, deploy branches, buildpack detect — plus three inline gaps: a viewer could widen their own API key's scopes, the demo-deploy flag was doing double duty as an authorization one, and tagging a shared resource had no write check at all while attaching one did. `_ensure_group_mutable` only ever blocked *system* queue groups, so every queue_bus mutation now also calls `_ensure_group_accessible` (owner-or-admin) — renaming, draining or deleting another user's group was open to any authenticated caller. The rbac policy decorators authenticate through `auth_required()`, so they replace `@jwt_required()` rather than stack under it; stacking rejects API-key callers before the policy can authorize them (tests/test_auth_decorator_boundaries.py enforces this). Where a route then needed the caller it reads `get_current_user()` — `get_jwt_identity()` raises for an API-key request. The decorators also carry a `_sk_authz` marker so a static test can see the decision from the source. `migrations.get_migration_history` moves to `require_admin_user()` for the same reason its siblings did: the inline `User.ROLE_ADMIN` check it kept was already reading two names this module no longer imports, so the route raised NameError for every caller, admin included. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
186 lines
6.3 KiB
Python
186 lines
6.3 KiB
Python
from flask import Blueprint, request, jsonify
|
|
from flask_jwt_extended import jwt_required
|
|
from app.services.server_template_service import ServerTemplateService
|
|
from app.services.audit_service import AuditService
|
|
from app.models.audit_log import AuditLog
|
|
from app.middleware.rbac import admin_required, get_current_user
|
|
|
|
server_templates_bp = Blueprint('server_templates', __name__)
|
|
|
|
|
|
@server_templates_bp.route('/', methods=['GET'])
|
|
@jwt_required()
|
|
def list_templates():
|
|
category = request.args.get('category')
|
|
templates = ServerTemplateService.list_templates(category=category)
|
|
return jsonify({'templates': [t.to_dict() for t in templates]})
|
|
|
|
|
|
@server_templates_bp.route('/library', methods=['GET'])
|
|
@jwt_required()
|
|
def get_library():
|
|
return jsonify({'templates': ServerTemplateService.get_library_templates()})
|
|
|
|
|
|
@server_templates_bp.route('/library/<key>', methods=['POST'])
|
|
@jwt_required()
|
|
def create_from_library(key):
|
|
user = get_current_user()
|
|
if not user or not user.is_admin:
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
try:
|
|
template = ServerTemplateService.create_from_library(key, user.id)
|
|
return jsonify(template.to_dict()), 201
|
|
except ValueError as e:
|
|
return jsonify({'error': str(e)}), 400
|
|
|
|
|
|
@server_templates_bp.route('/<int:template_id>', methods=['GET'])
|
|
@jwt_required()
|
|
def get_template(template_id):
|
|
template = ServerTemplateService.get_template(template_id)
|
|
if not template:
|
|
return jsonify({'error': 'Template not found'}), 404
|
|
return jsonify(template.to_dict())
|
|
|
|
|
|
@server_templates_bp.route('/', methods=['POST'])
|
|
@jwt_required()
|
|
def create_template():
|
|
user = get_current_user()
|
|
if not user or not user.is_admin:
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
data = request.get_json()
|
|
if not data or 'name' not in data:
|
|
return jsonify({'error': 'name required'}), 400
|
|
|
|
try:
|
|
template = ServerTemplateService.create_template(data, user.id)
|
|
AuditService.log(
|
|
action=AuditLog.ACTION_RESOURCE_CREATE, user_id=user.id,
|
|
target_type='server_template', target_id=template.id,
|
|
details={'name': template.name}
|
|
)
|
|
return jsonify(template.to_dict()), 201
|
|
except ValueError as e:
|
|
return jsonify({'error': str(e)}), 400
|
|
|
|
|
|
@server_templates_bp.route('/<int:template_id>', methods=['PUT'])
|
|
@jwt_required()
|
|
def update_template(template_id):
|
|
user = get_current_user()
|
|
if not user or not user.is_admin:
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
data = request.get_json()
|
|
template = ServerTemplateService.update_template(template_id, data)
|
|
if not template:
|
|
return jsonify({'error': 'Template not found'}), 404
|
|
return jsonify(template.to_dict())
|
|
|
|
|
|
@server_templates_bp.route('/<int:template_id>', methods=['DELETE'])
|
|
@jwt_required()
|
|
def delete_template(template_id):
|
|
user = get_current_user()
|
|
if not user or not user.is_admin:
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
try:
|
|
result = ServerTemplateService.delete_template(template_id)
|
|
if not result:
|
|
return jsonify({'error': 'Template not found'}), 404
|
|
return jsonify({'message': 'Template deleted'})
|
|
except ValueError as e:
|
|
return jsonify({'error': str(e)}), 400
|
|
|
|
|
|
# --- Assignments ---
|
|
|
|
@server_templates_bp.route('/<int:template_id>/assign', methods=['POST'])
|
|
@jwt_required()
|
|
def assign_template(template_id):
|
|
user = get_current_user()
|
|
if not user or not user.is_admin:
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
data = request.get_json() or {}
|
|
server_id = data.get('server_id')
|
|
if not server_id:
|
|
return jsonify({'error': 'server_id required'}), 400
|
|
|
|
try:
|
|
assignment = ServerTemplateService.assign_template(template_id, server_id)
|
|
return jsonify(assignment.to_dict()), 201
|
|
except ValueError as e:
|
|
return jsonify({'error': str(e)}), 400
|
|
|
|
|
|
@server_templates_bp.route('/<int:template_id>/bulk-assign', methods=['POST'])
|
|
@jwt_required()
|
|
def bulk_assign(template_id):
|
|
user = get_current_user()
|
|
if not user or not user.is_admin:
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
data = request.get_json() or {}
|
|
server_ids = data.get('server_ids', [])
|
|
results = ServerTemplateService.bulk_assign(template_id, server_ids)
|
|
return jsonify({'results': results})
|
|
|
|
|
|
@server_templates_bp.route('/<int:template_id>/assignments', methods=['GET'])
|
|
@jwt_required()
|
|
def get_assignments(template_id):
|
|
assignments = ServerTemplateService.get_template_assignments(template_id)
|
|
return jsonify({'assignments': [a.to_dict() for a in assignments]})
|
|
|
|
|
|
@server_templates_bp.route('/assignments/<int:assignment_id>', methods=['DELETE'])
|
|
@jwt_required()
|
|
def unassign(assignment_id):
|
|
user = get_current_user()
|
|
if not user or not user.is_admin:
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
result = ServerTemplateService.unassign_template(assignment_id)
|
|
if not result:
|
|
return jsonify({'error': 'Assignment not found'}), 404
|
|
return jsonify({'message': 'Template unassigned'})
|
|
|
|
|
|
@server_templates_bp.route('/assignments/<int:assignment_id>/check', methods=['POST'])
|
|
@admin_required
|
|
def check_drift(assignment_id):
|
|
assignment = ServerTemplateService.check_drift(assignment_id)
|
|
if not assignment:
|
|
return jsonify({'error': 'Assignment not found'}), 404
|
|
return jsonify(assignment.to_dict())
|
|
|
|
|
|
@server_templates_bp.route('/assignments/<int:assignment_id>/remediate', methods=['POST'])
|
|
@jwt_required()
|
|
def remediate(assignment_id):
|
|
user = get_current_user()
|
|
if not user or not user.is_admin:
|
|
return jsonify({'error': 'Admin access required'}), 403
|
|
|
|
assignment = ServerTemplateService.remediate(assignment_id)
|
|
if not assignment:
|
|
return jsonify({'error': 'Assignment not found'}), 404
|
|
return jsonify(assignment.to_dict())
|
|
|
|
|
|
@server_templates_bp.route('/compliance', methods=['GET'])
|
|
@jwt_required()
|
|
def compliance_summary():
|
|
summary = ServerTemplateService.get_compliance_summary()
|
|
return jsonify(summary)
|
|
|
|
|
|
@server_templates_bp.route('/server/<int:server_id>', methods=['GET'])
|
|
@jwt_required()
|
|
def get_server_templates(server_id):
|
|
assignments = ServerTemplateService.get_server_assignments(server_id)
|
|
return jsonify({'assignments': [a.to_dict() for a in assignments]})
|