# Bucket: PER-APP. Env-var routes converge on the app-access seam: # reads use can_access_app (owner/admin/any grant — a viewer grant can read, # with secrets still masked), writes use can_edit_app (owner/admin/editor grant). """ Environment Variables API Provides endpoints for managing application environment variables. All values are encrypted at rest. """ from flask import Blueprint, request, jsonify from flask_jwt_extended import jwt_required, get_jwt_identity from app.services.env_service import EnvService from app.services.resource_grant_service import ResourceGrantService from app.api._access import require_app from app.middleware.rbac import get_current_user from app.api._request import json_object env_vars_bp = Blueprint('env_vars', __name__) @env_vars_bp.route('//env', methods=['GET']) @jwt_required() def get_env_vars(app_id): """Get all environment variables for an application.""" user = get_current_user() app = require_app(app_id) if request.args.get('view') == 'effective': service = request.args.get('service') or None if service is not None and (len(service) > 255 or not all( c.isalnum() or c in '._-' for c in service)): return jsonify({'error': 'Invalid service name'}), 400 return jsonify(EnvService.get_effective_env_view(app_id, service=service)), 200 # A read-only grant never sees secret values, whatever ?mask says. mask_secrets = (request.args.get('mask', 'false').lower() == 'true' or not ResourceGrantService.can_edit_app(user, app)) env_vars = EnvService.get_env_vars(app_id, mask_secrets=mask_secrets) return jsonify({ 'env_vars': env_vars, 'count': len(env_vars) }), 200 @env_vars_bp.route('//env', methods=['POST']) @jwt_required() def create_env_var(app_id): """Create a new environment variable.""" current_user_id = get_jwt_identity() app = require_app(app_id, write=True) data = json_object() if not data: return jsonify({'error': 'No data provided'}), 400 key = data.get('key') value = data.get('value', '') is_secret = data.get('is_secret', False) description = data.get('description') target_service = data.get('target_service') if not key: return jsonify({'error': 'Key is required'}), 400 env_var, created, err = EnvService.set_env_var( app_id, key, value, is_secret, description, current_user_id, target_service=target_service ) if err: return jsonify({'error': err}), 400 return jsonify({ 'message': 'Environment variable created' if created else 'Environment variable updated', 'env_var': env_var.to_dict(), 'created': created }), 201 if created else 200 @env_vars_bp.route('//env/', methods=['GET']) @jwt_required() def get_env_var(app_id, key): """Get a single environment variable by key.""" user = get_current_user() app = require_app(app_id) env_var = EnvService.get_env_var(app_id, key) if not env_var: return jsonify({'error': 'Environment variable not found'}), 404 return jsonify({ 'env_var': env_var.to_dict( mask_secrets=not ResourceGrantService.can_edit_app(user, app)) }), 200 @env_vars_bp.route('//env/', methods=['PUT']) @jwt_required() def update_env_var(app_id, key): """Update an existing environment variable.""" current_user_id = get_jwt_identity() app = require_app(app_id, write=True) data = json_object() if not data: return jsonify({'error': 'No data provided'}), 400 updates = {} if data.get('value') is not None: updates['value'] = data['value'] if data.get('is_secret') is not None: updates['is_secret'] = data['is_secret'] # Key-presence, not non-null: an explicit JSON null clears the # description (the service's _UNSET sentinel keeps absent = untouched). if 'description' in data: updates['description'] = data.get('description') if 'target_service' in data: updates['target_service'] = data.get('target_service') existing, err = EnvService.update_env_var( app_id, key, user_id=current_user_id, **updates, ) if err: return jsonify({'error': err}), 404 return jsonify({ 'message': 'Environment variable updated', 'env_var': existing.to_dict() }), 200 @env_vars_bp.route('//env/', methods=['DELETE']) @jwt_required() def delete_env_var(app_id, key): """Delete an environment variable.""" current_user_id = get_jwt_identity() app = require_app(app_id, write=True) success, err = EnvService.delete_env_var(app_id, key, current_user_id) if not success: return jsonify({'error': err}), 404 return jsonify({ 'message': 'Environment variable deleted', 'key': key }), 200 @env_vars_bp.route('//env/bulk', methods=['POST']) @jwt_required() def bulk_set_env_vars(app_id): """Set multiple environment variables at once.""" current_user_id = get_jwt_identity() app = require_app(app_id, write=True) data = json_object() if not data or 'env_vars' not in data: return jsonify({'error': 'env_vars object is required'}), 400 count, errors = EnvService.bulk_set_env_vars( app_id, data['env_vars'], current_user_id ) return jsonify({ 'message': f'{count} environment variables set', 'count': count, 'errors': errors if errors else None }), 200 if not errors else 207 # 207 Multi-Status if partial success @env_vars_bp.route('//env/import', methods=['POST']) @jwt_required() def import_env_file(app_id): """Import environment variables from .env file content.""" current_user_id = get_jwt_identity() app = require_app(app_id, write=True) data = json_object() if not data or 'content' not in data: return jsonify({'error': 'content is required'}), 400 # Parse the .env content env_vars, parse_errors = EnvService.parse_env_file(data['content']) if not env_vars and parse_errors: return jsonify({ 'error': 'Failed to parse .env content', 'parse_errors': parse_errors }), 400 # Whether to overwrite existing overwrite = data.get('overwrite', True) if not overwrite: # Filter out existing keys existing_keys = {ev.key for ev in EnvService.get_env_vars(app_id)} env_vars = {k: v for k, v in env_vars.items() if k not in existing_keys} # Import the variables count, errors = EnvService.bulk_set_env_vars(app_id, env_vars, current_user_id) return jsonify({ 'message': f'{count} environment variables imported', 'count': count, 'parse_errors': parse_errors if parse_errors else None, 'import_errors': errors if errors else None }), 200 @env_vars_bp.route('//env/export', methods=['GET']) @jwt_required() def export_env_file(app_id): """Export environment variables as .env file content.""" user = get_current_user() app = require_app(app_id) include_secrets = (request.args.get('include_secrets', 'true').lower() == 'true' and ResourceGrantService.can_edit_app(user, app)) content = EnvService.export_to_env_format(app_id, include_secrets=include_secrets) return jsonify({ 'content': content, 'filename': f'{app.name}.env' }), 200 @env_vars_bp.route('//env/history', methods=['GET']) @jwt_required() def get_env_history(app_id): """Get change history for environment variables.""" app = require_app(app_id) limit = request.args.get('limit', 50, type=int) history = EnvService.get_history(app_id, limit=limit) return jsonify({ 'history': history, 'count': len(history) }), 200 @env_vars_bp.route('//env/clear', methods=['DELETE']) @jwt_required() def clear_all_env_vars(app_id): """Delete all environment variables for an application.""" current_user_id = get_jwt_identity() app = require_app(app_id, write=True) count = EnvService.clear_all(app_id, current_user_id) return jsonify({ 'message': f'{count} environment variables deleted', 'count': count }), 200