mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-27 18:01:58 -06:00
Incorporated Script Code-Signing
This commit is contained in:
@@ -8,6 +8,7 @@ import base64
|
|||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
from PyQt5 import QtWidgets, QtGui
|
from PyQt5 import QtWidgets, QtGui
|
||||||
|
|
||||||
|
from signature_utils import decode_script_bytes, verify_and_store_script_signature
|
||||||
|
|
||||||
ROLE_NAME = 'script_exec_currentuser'
|
ROLE_NAME = 'script_exec_currentuser'
|
||||||
ROLE_CONTEXTS = ['interactive']
|
ROLE_CONTEXTS = ['interactive']
|
||||||
@@ -277,7 +278,55 @@ class Role:
|
|||||||
job_id = payload.get('job_id')
|
job_id = payload.get('job_id')
|
||||||
script_type = (payload.get('script_type') or '').lower()
|
script_type = (payload.get('script_type') or '').lower()
|
||||||
run_mode = (payload.get('run_mode') or 'current_user').lower()
|
run_mode = (payload.get('run_mode') or 'current_user').lower()
|
||||||
content = _decode_script_content(payload.get('script_content'), payload.get('script_encoding'))
|
if run_mode == 'system':
|
||||||
|
return
|
||||||
|
script_bytes = decode_script_bytes(payload.get('script_content'), payload.get('script_encoding'))
|
||||||
|
if script_bytes is None:
|
||||||
|
await sio.emit('quick_job_result', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'status': 'Failed',
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': 'Invalid script payload (unable to decode)',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
signature_b64 = payload.get('signature')
|
||||||
|
sig_alg = (payload.get('sig_alg') or 'ed25519').lower()
|
||||||
|
signing_key = payload.get('signing_key')
|
||||||
|
if sig_alg and sig_alg not in ('ed25519', 'eddsa'):
|
||||||
|
await sio.emit('quick_job_result', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'status': 'Failed',
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': f'Unsupported script signature algorithm: {sig_alg}',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
if not isinstance(signature_b64, str) or not signature_b64.strip():
|
||||||
|
await sio.emit('quick_job_result', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'status': 'Failed',
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': 'Missing script signature; rejecting payload',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
http_client_fn = getattr(self.ctx, 'hooks', {}).get('http_client') if hasattr(self.ctx, 'hooks') else None
|
||||||
|
client = http_client_fn() if callable(http_client_fn) else None
|
||||||
|
if client is None:
|
||||||
|
await sio.emit('quick_job_result', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'status': 'Failed',
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': 'Signature verification unavailable (client missing)',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
if not verify_and_store_script_signature(client, script_bytes, signature_b64, signing_key):
|
||||||
|
await sio.emit('quick_job_result', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'status': 'Failed',
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': 'Rejected script payload due to invalid signature',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
content = script_bytes.decode('utf-8', errors='replace')
|
||||||
raw_env = payload.get('environment')
|
raw_env = payload.get('environment')
|
||||||
env_map = _sanitize_env_map(raw_env)
|
env_map = _sanitize_env_map(raw_env)
|
||||||
variables = payload.get('variables') if isinstance(payload.get('variables'), list) else []
|
variables = payload.get('variables') if isinstance(payload.get('variables'), list) else []
|
||||||
@@ -302,8 +351,6 @@ class Role:
|
|||||||
timeout_seconds = max(0, int(payload.get('timeout_seconds') or 0))
|
timeout_seconds = max(0, int(payload.get('timeout_seconds') or 0))
|
||||||
except Exception:
|
except Exception:
|
||||||
timeout_seconds = 0
|
timeout_seconds = 0
|
||||||
if run_mode == 'system':
|
|
||||||
return
|
|
||||||
if script_type != 'powershell':
|
if script_type != 'powershell':
|
||||||
await sio.emit('quick_job_result', { 'job_id': job_id, 'status': 'Failed', 'stdout': '', 'stderr': f"Unsupported type: {script_type}" })
|
await sio.emit('quick_job_result', { 'job_id': job_id, 'status': 'Failed', 'stdout': '', 'stderr': f"Unsupported type: {script_type}" })
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import subprocess
|
|||||||
import base64
|
import base64
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from signature_utils import decode_script_bytes, verify_and_store_script_signature
|
||||||
|
|
||||||
|
|
||||||
ROLE_NAME = 'script_exec_system'
|
ROLE_NAME = 'script_exec_system'
|
||||||
ROLE_CONTEXTS = ['system']
|
ROLE_CONTEXTS = ['system']
|
||||||
@@ -293,7 +295,53 @@ class Role:
|
|||||||
return
|
return
|
||||||
job_id = payload.get('job_id')
|
job_id = payload.get('job_id')
|
||||||
script_type = (payload.get('script_type') or '').lower()
|
script_type = (payload.get('script_type') or '').lower()
|
||||||
content = _decode_script_content(payload.get('script_content'), payload.get('script_encoding'))
|
script_bytes = decode_script_bytes(payload.get('script_content'), payload.get('script_encoding'))
|
||||||
|
if script_bytes is None:
|
||||||
|
await sio.emit('quick_job_result', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'status': 'Failed',
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': 'Invalid script payload (unable to decode)',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
signature_b64 = payload.get('signature')
|
||||||
|
sig_alg = (payload.get('sig_alg') or 'ed25519').lower()
|
||||||
|
signing_key = payload.get('signing_key')
|
||||||
|
if sig_alg and sig_alg not in ('ed25519', 'eddsa'):
|
||||||
|
await sio.emit('quick_job_result', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'status': 'Failed',
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': f'Unsupported script signature algorithm: {sig_alg}',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
if not isinstance(signature_b64, str) or not signature_b64.strip():
|
||||||
|
await sio.emit('quick_job_result', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'status': 'Failed',
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': 'Missing script signature; rejecting payload',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
http_client_fn = getattr(self.ctx, 'hooks', {}).get('http_client') if hasattr(self.ctx, 'hooks') else None
|
||||||
|
client = http_client_fn() if callable(http_client_fn) else None
|
||||||
|
if client is None:
|
||||||
|
await sio.emit('quick_job_result', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'status': 'Failed',
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': 'Signature verification unavailable (client missing)',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
if not verify_and_store_script_signature(client, script_bytes, signature_b64, signing_key):
|
||||||
|
await sio.emit('quick_job_result', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'status': 'Failed',
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': 'Rejected script payload due to invalid signature',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
content = script_bytes.decode('utf-8', errors='replace')
|
||||||
raw_env = payload.get('environment')
|
raw_env = payload.get('environment')
|
||||||
env_map = _sanitize_env_map(raw_env)
|
env_map = _sanitize_env_map(raw_env)
|
||||||
variables = payload.get('variables') if isinstance(payload.get('variables'), list) else []
|
variables = payload.get('variables') if isinstance(payload.get('variables'), list) else []
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import aiohttp
|
|||||||
|
|
||||||
import socketio
|
import socketio
|
||||||
from security import AgentKeyStore
|
from security import AgentKeyStore
|
||||||
|
from signature_utils import decode_script_bytes as _decode_script_bytes, verify_and_store_script_signature as _verify_and_store_script_signature
|
||||||
|
|
||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||||
@@ -2339,47 +2340,6 @@ async def send_heartbeat():
|
|||||||
await asyncio.sleep(60)
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
|
||||||
def _verify_and_store_script_signature(
|
|
||||||
client: AgentHttpClient,
|
|
||||||
script_bytes: bytes,
|
|
||||||
signature_b64: str,
|
|
||||||
signing_key_hint: Optional[str] = None,
|
|
||||||
) -> bool:
|
|
||||||
candidates: List[str] = []
|
|
||||||
if isinstance(signing_key_hint, str) and signing_key_hint.strip():
|
|
||||||
candidates.append(signing_key_hint.strip())
|
|
||||||
stored_key = client.load_server_signing_key()
|
|
||||||
if stored_key:
|
|
||||||
key_text = stored_key.strip()
|
|
||||||
if key_text and key_text not in candidates:
|
|
||||||
candidates.append(key_text)
|
|
||||||
for key_b64 in candidates:
|
|
||||||
try:
|
|
||||||
key_der = base64.b64decode(key_b64, validate=True)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
public_key = serialization.load_der_public_key(key_der)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
if not isinstance(public_key, ed25519.Ed25519PublicKey):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
signature = base64.b64decode(signature_b64, validate=True)
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
public_key.verify(signature, script_bytes)
|
|
||||||
if stored_key and stored_key.strip() != key_b64:
|
|
||||||
client.store_server_signing_key(key_b64)
|
|
||||||
elif not stored_key:
|
|
||||||
client.store_server_signing_key(key_b64)
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
async def poll_script_requests():
|
async def poll_script_requests():
|
||||||
await asyncio.sleep(20)
|
await asyncio.sleep(20)
|
||||||
client = http_client()
|
client = http_client()
|
||||||
@@ -2394,9 +2354,9 @@ async def poll_script_requests():
|
|||||||
signature_b64 = response.get("signature")
|
signature_b64 = response.get("signature")
|
||||||
sig_alg = (response.get("sig_alg") or "").lower()
|
sig_alg = (response.get("sig_alg") or "").lower()
|
||||||
if script_b64 and signature_b64:
|
if script_b64 and signature_b64:
|
||||||
script_bytes = _decode_base64_bytes(script_b64)
|
script_bytes = _decode_script_bytes(script_b64, "base64")
|
||||||
if script_bytes is None:
|
if script_bytes is None:
|
||||||
_log_agent('received script payload with invalid base64 encoding', fname='agent.error.log')
|
_log_agent('received script payload with invalid encoding', fname='agent.error.log')
|
||||||
elif sig_alg and sig_alg not in ("ed25519", "eddsa"):
|
elif sig_alg and sig_alg not in ("ed25519", "eddsa"):
|
||||||
_log_agent(f'unsupported script signature algorithm: {sig_alg}', fname='agent.error.log')
|
_log_agent(f'unsupported script signature algorithm: {sig_alg}', fname='agent.error.log')
|
||||||
else:
|
else:
|
||||||
@@ -3233,8 +3193,54 @@ if __name__=='__main__':
|
|||||||
return
|
return
|
||||||
job_id = payload.get('job_id')
|
job_id = payload.get('job_id')
|
||||||
script_type = (payload.get('script_type') or '').lower()
|
script_type = (payload.get('script_type') or '').lower()
|
||||||
content = _decode_script_payload(payload.get('script_content'), payload.get('script_encoding'))
|
encoding_hint = payload.get('script_encoding')
|
||||||
|
script_bytes = _decode_script_bytes(payload.get('script_content'), encoding_hint)
|
||||||
run_mode = (payload.get('run_mode') or 'current_user').lower()
|
run_mode = (payload.get('run_mode') or 'current_user').lower()
|
||||||
|
if script_bytes is None:
|
||||||
|
err = 'Invalid script payload (unable to decode)'
|
||||||
|
await sio.emit('quick_job_result', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'status': 'Failed',
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': err,
|
||||||
|
})
|
||||||
|
_log_agent(err, fname='agent.error.log')
|
||||||
|
return
|
||||||
|
signature_b64 = payload.get('signature')
|
||||||
|
sig_alg = (payload.get('sig_alg') or 'ed25519').lower()
|
||||||
|
signing_key = payload.get('signing_key')
|
||||||
|
if sig_alg and sig_alg not in ('ed25519', 'eddsa'):
|
||||||
|
err = f"Unsupported script signature algorithm: {sig_alg}"
|
||||||
|
await sio.emit('quick_job_result', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'status': 'Failed',
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': err,
|
||||||
|
})
|
||||||
|
_log_agent(err, fname='agent.error.log')
|
||||||
|
return
|
||||||
|
if not isinstance(signature_b64, str) or not signature_b64.strip():
|
||||||
|
err = 'Missing script signature; rejecting payload'
|
||||||
|
await sio.emit('quick_job_result', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'status': 'Failed',
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': err,
|
||||||
|
})
|
||||||
|
_log_agent(err, fname='agent.error.log')
|
||||||
|
return
|
||||||
|
client = http_client()
|
||||||
|
if not _verify_and_store_script_signature(client, script_bytes, signature_b64, signing_key):
|
||||||
|
err = 'Rejected script payload due to invalid signature'
|
||||||
|
await sio.emit('quick_job_result', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'status': 'Failed',
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': err,
|
||||||
|
})
|
||||||
|
_log_agent(err, fname='agent.error.log')
|
||||||
|
return
|
||||||
|
content = script_bytes.decode('utf-8', errors='replace')
|
||||||
if script_type != 'powershell':
|
if script_type != 'powershell':
|
||||||
await sio.emit('quick_job_result', { 'job_id': job_id, 'status': 'Failed', 'stdout': '', 'stderr': f"Unsupported type: {script_type}" })
|
await sio.emit('quick_job_result', { 'job_id': job_id, 'status': 'Failed', 'stdout': '', 'stderr': f"Unsupported type: {script_type}" })
|
||||||
return
|
return
|
||||||
|
|||||||
119
Data/Agent/signature_utils.py
Normal file
119
Data/Agent/signature_utils.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from typing import Any, Optional, Sequence
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||||
|
|
||||||
|
_BASE64_HINTS: Sequence[str] = ("base64", "b64", "base-64")
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_base64_bytes(value: Optional[str]) -> Optional[bytes]:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return None
|
||||||
|
stripped = value.strip()
|
||||||
|
if not stripped:
|
||||||
|
return b""
|
||||||
|
cleaned = "".join(stripped.split())
|
||||||
|
if not cleaned:
|
||||||
|
return b""
|
||||||
|
try:
|
||||||
|
return base64.b64decode(cleaned, validate=True)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def decode_script_bytes(script_content: Any, encoding_hint: Optional[str]) -> Optional[bytes]:
|
||||||
|
"""
|
||||||
|
Normalize a script payload into UTF-8 bytes.
|
||||||
|
|
||||||
|
Returns None only when the payload is unusable (e.g., invalid base64,
|
||||||
|
unsupported type). Empty content is represented as b"".
|
||||||
|
"""
|
||||||
|
if isinstance(script_content, (bytes, bytearray)):
|
||||||
|
return bytes(script_content)
|
||||||
|
|
||||||
|
if script_content is None:
|
||||||
|
return b""
|
||||||
|
|
||||||
|
encoding = str(encoding_hint or "").strip().lower()
|
||||||
|
if isinstance(script_content, str):
|
||||||
|
if encoding in _BASE64_HINTS:
|
||||||
|
decoded = _decode_base64_bytes(script_content)
|
||||||
|
return decoded
|
||||||
|
|
||||||
|
decoded = _decode_base64_bytes(script_content)
|
||||||
|
if decoded is not None:
|
||||||
|
return decoded
|
||||||
|
|
||||||
|
try:
|
||||||
|
return script_content.encode("utf-8")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def verify_and_store_script_signature(
|
||||||
|
client: Any,
|
||||||
|
script_bytes: bytes,
|
||||||
|
signature_b64: Optional[str],
|
||||||
|
signing_key_hint: Optional[str] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Verify a script payload against the provided signature and persist the server
|
||||||
|
signing key when verification succeeds.
|
||||||
|
"""
|
||||||
|
if not isinstance(script_bytes, (bytes, bytearray)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
signature = _decode_base64_bytes(signature_b64)
|
||||||
|
if signature is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
if isinstance(signing_key_hint, str):
|
||||||
|
hint = signing_key_hint.strip()
|
||||||
|
if hint:
|
||||||
|
candidates.append(hint)
|
||||||
|
|
||||||
|
stored_key = None
|
||||||
|
if client is not None and hasattr(client, "load_server_signing_key"):
|
||||||
|
try:
|
||||||
|
stored_key_raw = client.load_server_signing_key()
|
||||||
|
except Exception:
|
||||||
|
stored_key_raw = None
|
||||||
|
if isinstance(stored_key_raw, str):
|
||||||
|
stored_key = stored_key_raw.strip()
|
||||||
|
if stored_key and stored_key not in candidates:
|
||||||
|
candidates.append(stored_key)
|
||||||
|
|
||||||
|
payload = bytes(script_bytes)
|
||||||
|
for key_b64 in candidates:
|
||||||
|
try:
|
||||||
|
key_der = base64.b64decode(key_b64, validate=True)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
public_key = serialization.load_der_public_key(key_der)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if not isinstance(public_key, ed25519.Ed25519PublicKey):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
public_key.verify(signature, payload)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if client is not None and hasattr(client, "store_server_signing_key"):
|
||||||
|
try:
|
||||||
|
if stored_key and stored_key != key_b64:
|
||||||
|
client.store_server_signing_key(key_b64)
|
||||||
|
elif not stored_key:
|
||||||
|
client.store_server_signing_key(key_b64)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
@@ -322,10 +322,11 @@ def _to_dt_tuple(ts: int) -> Tuple[int, int, int, int, int, int]:
|
|||||||
|
|
||||||
|
|
||||||
class JobScheduler:
|
class JobScheduler:
|
||||||
def __init__(self, app, socketio, db_path: str):
|
def __init__(self, app, socketio, db_path: str, script_signer=None):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.socketio = socketio
|
self.socketio = socketio
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
|
self._script_signer = script_signer
|
||||||
self._running = False
|
self._running = False
|
||||||
# Simulated run duration to hold jobs in "Running" before Success
|
# Simulated run duration to hold jobs in "Running" before Success
|
||||||
self.SIMULATED_RUN_SECONDS = int(os.environ.get("BOREALIS_SIM_RUN_SECONDS", "30"))
|
self.SIMULATED_RUN_SECONDS = int(os.environ.get("BOREALIS_SIM_RUN_SECONDS", "30"))
|
||||||
@@ -545,7 +546,22 @@ class JobScheduler:
|
|||||||
return None
|
return None
|
||||||
doc = self._load_assembly_document(abs_path, "ansible")
|
doc = self._load_assembly_document(abs_path, "ansible")
|
||||||
content = doc.get("script") or ""
|
content = doc.get("script") or ""
|
||||||
encoded_content = _encode_script_content(content)
|
normalized_script = (content or "").replace("\r\n", "\n")
|
||||||
|
script_bytes = normalized_script.encode("utf-8")
|
||||||
|
encoded_content = base64.b64encode(script_bytes).decode("ascii") if script_bytes or normalized_script == "" else ""
|
||||||
|
signature_b64: Optional[str] = None
|
||||||
|
sig_alg: Optional[str] = None
|
||||||
|
signing_key_b64: Optional[str] = None
|
||||||
|
if self._script_signer is not None:
|
||||||
|
try:
|
||||||
|
signature = self._script_signer.sign(script_bytes)
|
||||||
|
signature_b64 = base64.b64encode(signature).decode("ascii")
|
||||||
|
sig_alg = "ed25519"
|
||||||
|
signing_key_b64 = self._script_signer.public_base64_spki()
|
||||||
|
except Exception:
|
||||||
|
signature_b64 = None
|
||||||
|
sig_alg = None
|
||||||
|
signing_key_b64 = None
|
||||||
variables = doc.get("variables") or []
|
variables = doc.get("variables") or []
|
||||||
files = doc.get("files") or []
|
files = doc.get("files") or []
|
||||||
run_mode_norm = (run_mode or "system").strip().lower()
|
run_mode_norm = (run_mode or "system").strip().lower()
|
||||||
@@ -765,6 +781,12 @@ class JobScheduler:
|
|||||||
"admin_user": "",
|
"admin_user": "",
|
||||||
"admin_pass": "",
|
"admin_pass": "",
|
||||||
}
|
}
|
||||||
|
if signature_b64:
|
||||||
|
payload["signature"] = signature_b64
|
||||||
|
if sig_alg:
|
||||||
|
payload["sig_alg"] = sig_alg
|
||||||
|
if signing_key_b64:
|
||||||
|
payload["signing_key"] = signing_key_b64
|
||||||
try:
|
try:
|
||||||
self.socketio.emit("quick_job_run", payload)
|
self.socketio.emit("quick_job_run", payload)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1799,9 +1821,9 @@ class JobScheduler:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def register(app, socketio, db_path: str) -> JobScheduler:
|
def register(app, socketio, db_path: str, script_signer=None) -> JobScheduler:
|
||||||
"""Factory to create and return a JobScheduler instance."""
|
"""Factory to create and return a JobScheduler instance."""
|
||||||
return JobScheduler(app, socketio, db_path)
|
return JobScheduler(app, socketio, db_path, script_signer=script_signer)
|
||||||
|
|
||||||
|
|
||||||
def set_online_lookup(scheduler: JobScheduler, fn: Callable[[], List[str]]):
|
def set_online_lookup(scheduler: JobScheduler, fn: Callable[[], List[str]]):
|
||||||
|
|||||||
@@ -5153,7 +5153,7 @@ ensure_default_admin()
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Connect the Flask app to the background job scheduler and helpers.
|
# Connect the Flask app to the background job scheduler and helpers.
|
||||||
|
|
||||||
job_scheduler = register_job_scheduler(app, socketio, DB_PATH)
|
job_scheduler = register_job_scheduler(app, socketio, DB_PATH, script_signer=SCRIPT_SIGNER)
|
||||||
scheduler_set_server_runner(job_scheduler, _queue_server_ansible_run)
|
scheduler_set_server_runner(job_scheduler, _queue_server_ansible_run)
|
||||||
scheduler_set_credential_fetcher(job_scheduler, _fetch_credential_with_secrets)
|
scheduler_set_credential_fetcher(job_scheduler, _fetch_credential_with_secrets)
|
||||||
job_scheduler.start()
|
job_scheduler.start()
|
||||||
@@ -6922,7 +6922,19 @@ def scripts_quick_run():
|
|||||||
|
|
||||||
env_map, variables, literal_lookup = _prepare_variable_context(doc_variables, overrides)
|
env_map, variables, literal_lookup = _prepare_variable_context(doc_variables, overrides)
|
||||||
content = _rewrite_powershell_script(content, literal_lookup)
|
content = _rewrite_powershell_script(content, literal_lookup)
|
||||||
encoded_content = _encode_script_content(content)
|
normalized_script = (content or "").replace("\r\n", "\n")
|
||||||
|
script_bytes = normalized_script.encode("utf-8")
|
||||||
|
encoded_content = base64.b64encode(script_bytes).decode("ascii") if script_bytes or normalized_script == "" else ""
|
||||||
|
signature_b64 = ""
|
||||||
|
signing_key_b64 = ""
|
||||||
|
if SCRIPT_SIGNER:
|
||||||
|
try:
|
||||||
|
signature_raw = SCRIPT_SIGNER.sign(script_bytes)
|
||||||
|
signature_b64 = base64.b64encode(signature_raw).decode("ascii")
|
||||||
|
signing_key_b64 = SCRIPT_SIGNER.public_base64_spki()
|
||||||
|
except Exception:
|
||||||
|
signature_b64 = ""
|
||||||
|
signing_key_b64 = ""
|
||||||
timeout_seconds = 0
|
timeout_seconds = 0
|
||||||
try:
|
try:
|
||||||
timeout_seconds = max(0, int(doc.get("timeout_seconds") or 0))
|
timeout_seconds = max(0, int(doc.get("timeout_seconds") or 0))
|
||||||
@@ -6975,6 +6987,11 @@ def scripts_quick_run():
|
|||||||
"admin_user": admin_user,
|
"admin_user": admin_user,
|
||||||
"admin_pass": admin_pass,
|
"admin_pass": admin_pass,
|
||||||
}
|
}
|
||||||
|
if signature_b64:
|
||||||
|
payload["signature"] = signature_b64
|
||||||
|
payload["sig_alg"] = "ed25519"
|
||||||
|
if signing_key_b64:
|
||||||
|
payload["signing_key"] = signing_key_b64
|
||||||
# Broadcast to all connected clients; no broadcast kw in python-socketio v5
|
# Broadcast to all connected clients; no broadcast kw in python-socketio v5
|
||||||
socketio.emit("quick_job_run", payload)
|
socketio.emit("quick_job_run", payload)
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user