feat(exec): add template aliases, tolerant read-only validation, and thread titling filter
This commit is contained in:
+65
-3
@@ -1053,6 +1053,60 @@ OPS = {
|
||||
# identity -> set of ops. 'master' may invoke everything. Unknown identities
|
||||
# get the read-only subset. Per-agent tokens inherit their agent name as the
|
||||
# identity; tighten per agent here as needed.
|
||||
# ---- read-only tolerance + hyphenated aliases used by job templates ----
|
||||
_ARG_SYNONYMS = {'job': 'name', 'id': 'name', 'job_name': 'name', 'unit': 'name'}
|
||||
|
||||
|
||||
def _tolerant_args(spec, args):
|
||||
"""Read-only ops: drop unknown args (mapping common synonyms) instead of failing.
|
||||
Returns (args, dropped_names)."""
|
||||
if args is None:
|
||||
return {}, []
|
||||
if not isinstance(args, dict):
|
||||
return args, []
|
||||
args, dropped = dict(args), []
|
||||
for _ in range(len(args) + 1):
|
||||
try:
|
||||
spec['validate'](args)
|
||||
break
|
||||
except OpError as e:
|
||||
m = re.match(r'unknown arg: (\S+)', str(e))
|
||||
if m and m.group(1) in args:
|
||||
k = m.group(1)
|
||||
v = args.pop(k)
|
||||
syn = _ARG_SYNONYMS.get(k)
|
||||
if syn and syn not in args:
|
||||
args[syn] = v
|
||||
else:
|
||||
dropped.append(k)
|
||||
continue
|
||||
if 'takes no' in str(e):
|
||||
dropped.extend(args)
|
||||
args = {}
|
||||
continue
|
||||
break
|
||||
return args, dropped
|
||||
|
||||
|
||||
def _register_aliases():
|
||||
alias = {'job-list': 'cron.runs', 'quality.validate': 'quality.check',
|
||||
'vars-list': 'vars.list', 'fleet-status': 'health.check'}
|
||||
for new, old in alias.items():
|
||||
if new not in OPS and old in OPS:
|
||||
OPS[new] = dict(OPS[old], desc=OPS[old]['desc'] + f' (alias of {old})')
|
||||
def _ctl(*words):
|
||||
return lambda a: [sys.executable, os.path.join(BIN_DIR, 'box-ctl.py'), *words]
|
||||
def _noargs(raw):
|
||||
return {}
|
||||
for new, words, desc in (('loop-status', ('loop-status',), 'Fleet loop health (read-only)'),
|
||||
('timer-list', ('timer', 'list'), 'List box timers (read-only)')):
|
||||
if new not in OPS:
|
||||
OPS[new] = {'validate': _noargs, 'build': _ctl(*words), 'timeout': 30,
|
||||
'side_effecting': False, 'desc': desc}
|
||||
|
||||
|
||||
_register_aliases()
|
||||
|
||||
PERMISSIONS = {
|
||||
'master': set(OPS),
|
||||
'operator-main': set(OPS),
|
||||
@@ -1211,12 +1265,17 @@ class Handler(BaseHTTPRequestHandler):
|
||||
'op': op})
|
||||
self._send(403, {'error': 'forbidden for this identity'})
|
||||
return
|
||||
dropped = []
|
||||
try:
|
||||
spec = OPS[op]
|
||||
if not spec.get('side_effecting'):
|
||||
args, dropped = _tolerant_args(spec, args)
|
||||
clean = spec['validate'](args)
|
||||
argv = spec['build'](clean)
|
||||
except OpError as e:
|
||||
self._send(400, {'error': f'bad args: {e}'})
|
||||
self._send(400, {'error': f'bad args: {e}', 'op': op,
|
||||
'desc': OPS[op].get('desc', ''),
|
||||
'hint': 'GET /ops for the allowlist; see desc for usage'})
|
||||
return
|
||||
except Exception as e:
|
||||
sys.stderr.write(f'Validation/build exception for {op}: {e}\n')
|
||||
@@ -1247,12 +1306,15 @@ class Handler(BaseHTTPRequestHandler):
|
||||
audit({'event': 'exec_done', 'ident': ident, 'peer': peer,
|
||||
'op': op, 'rc': rc, 'duration_s': dt, 'ok': ok})
|
||||
if ok:
|
||||
self._send(200, {'rc': rc, 'stdout': out, 'stderr': err,
|
||||
'duration_s': dt})
|
||||
resp = {'rc': rc, 'stdout': out, 'stderr': err, 'duration_s': dt}
|
||||
if dropped:
|
||||
resp['dropped_args'] = dropped
|
||||
self._send(200, resp)
|
||||
else:
|
||||
self._send(500, {'error': err, 'rc': rc})
|
||||
|
||||
|
||||
|
||||
# ------------------------------------------------------- main
|
||||
|
||||
def main():
|
||||
|
||||
Reference in New Issue
Block a user