Add attended prune of retired bao.coulomb.social callbacks from platform-admin
Read-preserve-write via the reviewed loopback helper; keeps the tunnel callback and every other setting; also captures non-secret OIDC mount config. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 150322@bnt-lap001 Assistant-Session: 16a7b788-374e-4915-a1df-fc87ffd9a5e4
This commit is contained in:
parent
85f6a14892
commit
7f25af3cb1
3 changed files with 166 additions and 0 deletions
104
scripts/openbao_platform_admin_callback_prune.py
Executable file
104
scripts/openbao_platform_admin_callback_prune.py
Executable file
|
|
@ -0,0 +1,104 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Silent contained removal of the retired public callbacks from platform-admin.
|
||||
|
||||
Removes exactly the two bao.coulomb.social callbacks retired by RPF-WP-0025-T03
|
||||
from auth/netkingdom/role/platform-admin. Every other role setting is kept,
|
||||
using the reviewed read/payload/write helpers of the loopback-callback script.
|
||||
Refuses unless the tunnel callback is present and stays present. Writes
|
||||
nothing if the retired callbacks are already gone. One non-secret receipt; no
|
||||
output.
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
_spec = importlib.util.spec_from_file_location('loopback', HERE / 'openbao_operator_loopback_callback.py')
|
||||
loopback = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(loopback)
|
||||
|
||||
ROLE = loopback.ROLE
|
||||
KEEP = loopback.CALLBACK # http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback
|
||||
RETIRED = (
|
||||
'https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback',
|
||||
'https://bao.coulomb.social/ui/vault/auth/keycape/oidc/callback',
|
||||
)
|
||||
Refused = loopback.Refused
|
||||
MOUNTS = ('netkingdom', 'keycape')
|
||||
CONFIG_FIELDS = ('oidc_discovery_url', 'oidc_client_id', 'default_role', 'bound_issuer',
|
||||
'oidc_response_mode', 'oidc_response_types', 'jwt_supported_algs',
|
||||
'provider_config', 'namespace_in_state')
|
||||
|
||||
|
||||
def read_mount_configs():
|
||||
"""Non-secret OIDC mount settings; OpenBao never returns oidc_client_secret."""
|
||||
import subprocess
|
||||
configs = {}
|
||||
for mount in MOUNTS:
|
||||
try:
|
||||
result = subprocess.run(['bao', 'read', '-format=json', 'auth/%s/config' % mount],
|
||||
capture_output=True, check=True, timeout=30)
|
||||
data = json.loads(result.stdout)['data']
|
||||
configs[mount] = {key: data[key] for key in CONFIG_FIELDS if key in data}
|
||||
except Exception:
|
||||
configs[mount] = {'error': 'read_failed'}
|
||||
return configs
|
||||
|
||||
|
||||
def prune(read=loopback.read_role, write=loopback.write_role):
|
||||
original = read()
|
||||
uris = original['allowed_redirect_uris']
|
||||
if KEEP not in uris:
|
||||
raise Refused('tunnel_callback_missing')
|
||||
remaining = [uri for uri in uris if uri not in RETIRED]
|
||||
if remaining == uris:
|
||||
return False, uris
|
||||
desired = dict(loopback.payload(original), allowed_redirect_uris=remaining)
|
||||
if loopback.preserved(read()) != loopback.preserved(original):
|
||||
raise Refused('role_changed_before_write')
|
||||
write(desired)
|
||||
after = read()
|
||||
if sorted(after.get('allowed_redirect_uris', [])) != sorted(remaining):
|
||||
raise Refused('readback_callbacks_mismatch')
|
||||
if loopback.preserved(after) != loopback.preserved(original):
|
||||
raise Refused('readback_settings_changed')
|
||||
return True, after['allowed_redirect_uris']
|
||||
|
||||
|
||||
def write_receipt(path, status, **extra):
|
||||
fd = os.open(Path(path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
body = {
|
||||
'schema': 'railiance-platform.openbao-callback-prune.v1',
|
||||
'observed_at': datetime.now(timezone.utc).isoformat(),
|
||||
'role': ROLE, 'retired': list(RETIRED), 'status': status,
|
||||
'credential_values_emitted': False, **extra,
|
||||
}
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as handle:
|
||||
json.dump(body, handle, indent=2, sort_keys=True)
|
||||
handle.write('\n')
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 2 or argv[0] != '--receipt':
|
||||
return 2
|
||||
receipt = argv[1]
|
||||
try:
|
||||
loopback.require_attended()
|
||||
changed, uris = prune()
|
||||
write_receipt(receipt, 'pruned' if changed else 'already_pruned',
|
||||
changed=changed, allowed_redirect_uris=uris,
|
||||
mount_configs=read_mount_configs())
|
||||
return 0
|
||||
except Exception as error:
|
||||
try:
|
||||
write_receipt(receipt, loopback.classify(error), changed=False)
|
||||
except Exception:
|
||||
pass
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
Loading…
Add table
Add a link
Reference in a new issue