Prepare the openbao-admin signInDiagnosis flag rollout (NK-WP-0051-T02).
One-line, byte-preserving edit of sso/keycape-config with inspect, server dry-run, attended apply and an exact rollback. No values are emitted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 1041694@bnt-lap001 Assistant-Session: 7af0bb68-ac4a-4ce3-8639-24ddd84c0b52
This commit is contained in:
parent
916bc7d5b8
commit
c7ad8a5bf0
4 changed files with 355 additions and 18 deletions
|
|
@ -2,11 +2,18 @@
|
|||
# Custodian Brief — net-kingdom
|
||||
|
||||
**Domain:** infotech
|
||||
**Last synced:** 2026-10-14 21:54 UTC
|
||||
**Last synced:** 2026-10-14 22:11 UTC
|
||||
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
|
||||
|
||||
## Active Workstreams
|
||||
|
||||
### Implement deterministic posture and evidence feedback
|
||||
Progress: 4/5 done | workplan_id: `9d7b04f9-3803-5613-b7a5-8bd606c77f5a`
|
||||
|
||||
**Open tasks:**
|
||||
- ! Obtain audit-core freshness adoption `7d2029d2`
|
||||
*(wait: audit-core tenancy.yaml must publish structured authoritative owner, observation/expiry and replacement-action metadata for WH-ENG-20260822-AUDIT-E2-03. Re-evaluate with explicit current as-of; later recovery prose does not renew E2.)*
|
||||
|
||||
### Publish the NetKingdom emission-cadence security profile
|
||||
Progress: 4/5 done | workplan_id: `04685f94-1991-5e62-80d2-5669913e99fc`
|
||||
|
||||
|
|
@ -25,12 +32,13 @@ Progress: 3/6 done | workplan_id: `965ad365-6b81-50a1-a2a3-2d0c1fcce0b4`
|
|||
- ! T06 — Resolve the `DataClassification` mismatch `bf8b3e1d`
|
||||
*(wait: Requires InfoTechCanon ruling on public disclosure classification versus synthetic provenance, then ops-warden authoritative dataclass_floor mapping and tests; no public-to-synthetic alias.)*
|
||||
|
||||
### Implement deterministic posture and evidence feedback
|
||||
Progress: 4/5 done | workplan_id: `9d7b04f9-3803-5613-b7a5-8bd606c77f5a`
|
||||
### Remove bootstrap secret output and verify separated recovery custody
|
||||
Progress: 1/3 done | workplan_id: `17d315f0-829e-57c7-85cc-3a7914152804`
|
||||
|
||||
**Open tasks:**
|
||||
- ! Obtain audit-core freshness adoption `7d2029d2`
|
||||
*(wait: audit-core tenancy.yaml must publish structured authoritative owner, observation/expiry and replacement-action metadata for WH-ENG-20260822-AUDIT-E2-03. Re-evaluate with explicit current as-of; later recovery prose does not renew E2.)*
|
||||
- ! Verify owner-approved recovery and current native callers `d68cd4d3`
|
||||
*(wait: Source caller reconciliation and exact owner-scoped isolated recovery ceremony are required; no live bootstrap or root retrieval is authorized by this source receipt.)*
|
||||
- ► Reconcile other legacy custody callers and root-output boundaries `479d6970`
|
||||
|
||||
### Enterprise Federation & SAML — Expanded-Mode Keycloak Identity Broker
|
||||
Progress: 1/9 done | workplan_id: `1075448f-d533-5f9e-94b7-c3adfe151a07`
|
||||
|
|
@ -66,7 +74,7 @@ Progress: 2/6 done | workplan_id: `c1771c07-4466-5aa2-ad43-df63a471946b`
|
|||
|
||||
**Open tasks:**
|
||||
- ! KeyCape shows the principal before the callback `a78219bb`
|
||||
*(wait: Implemented in key-cape af3b1b0 (KEY-WP-0041). Waits for KEY-WP-0041-T02: image build, live signInDiagnosis flag on the OpenBao client, digest.)*
|
||||
*(wait: Implemented in key-cape af3b1b0 (KEY-WP-0041). Image cdec9dd7 live 2026-10-15. Waits for KEY-WP-0041-T02: live signInDiagnosis flag on the OpenBao client in sso/keycape-config.)*
|
||||
- ! Account site shows both sessions `bdc8b414`
|
||||
*(wait: Implemented in user-engine 80f5870 (USER-WP-0039). Waits for USER-WP-0039-T02: deploy and a mismatched-session check.)*
|
||||
- ! Enroll the negative-login factor `afb5816f`
|
||||
|
|
@ -74,13 +82,6 @@ Progress: 2/6 done | workplan_id: `c1771c07-4466-5aa2-ad43-df63a471946b`
|
|||
- ! Hand the OpenBao proof back to its owners `86fb4a6b`
|
||||
*(wait: Implemented in ops-mason fca5591 (MASON-WP-0012) and railiance-platform 0f0282a (RPF-WP-0057). Waits for CCR-2026-0045 apply, overlay deploy, and the MASON-WP-0012-T04 live proof.)*
|
||||
|
||||
### Define an execution-attribution receipt for Railiance runs
|
||||
Progress: 1/2 done | workplan_id: `e2533f3a-aa43-59b3-bff3-8e64b6149487`
|
||||
|
||||
**Open tasks:**
|
||||
- ! Agree the evidence holder and schema with audit-core and Railiance `963120c1`
|
||||
*(wait: Audit-core accepted evidence custody and published field/admission requirements in AUDIT-WP-0014-T01. Still needs Railiance owner confirmation of exact emitter and wire binding before canonical schema/validator implementation, followed by actual emission/readback and attribution/clock/unknown-case acceptance.)*
|
||||
|
||||
### Register Playwright human and agent consumers through governed identity delivery
|
||||
Progress: 1/3 done | workplan_id: `06ba98d5-609e-5aaa-9bf7-4c4f4460e377`
|
||||
|
||||
|
|
@ -89,13 +90,12 @@ Progress: 1/3 done | workplan_id: `06ba98d5-609e-5aaa-9bf7-4c4f4460e377`
|
|||
*(wait: Await owner-scoped registry/custody activation, native cluster authentication and individual transport, then real human and agent acceptance under CUST-WP-0080-T05.)*
|
||||
- ► Deliver custody integration and guarded registration activation `a9dadd07`
|
||||
|
||||
### Remove bootstrap secret output and verify separated recovery custody
|
||||
Progress: 1/3 done | workplan_id: `17d315f0-829e-57c7-85cc-3a7914152804`
|
||||
### Define an execution-attribution receipt for Railiance runs
|
||||
Progress: 1/2 done | workplan_id: `e2533f3a-aa43-59b3-bff3-8e64b6149487`
|
||||
|
||||
**Open tasks:**
|
||||
- ! Verify owner-approved recovery and current native callers `d68cd4d3`
|
||||
*(wait: Source caller reconciliation and exact owner-scoped isolated recovery ceremony are required; no live bootstrap or root retrieval is authorized by this source receipt.)*
|
||||
- ► Reconcile other legacy custody callers and root-output boundaries `479d6970`
|
||||
- ! Agree the evidence holder and schema with audit-core and Railiance `963120c1`
|
||||
*(wait: Audit-core accepted evidence custody and published field/admission requirements in AUDIT-WP-0014-T01. Still needs Railiance owner confirmation of exact emitter and wire binding before canonical schema/validator implementation, followed by actual emission/readback and attribution/clock/unknown-case acceptance.)*
|
||||
|
||||
### Cut over NetKingdom identity to railiance01 and retire CoulombCore
|
||||
Progress: 7/8 done | workplan_id: `d76ddccc-00c8-548a-b141-2cd660fa38da`
|
||||
|
|
|
|||
176
sso-mfa/k8s/keycape/openbao-signin-diagnosis-rollout.py
Normal file
176
sso-mfa/k8s/keycape/openbao-signin-diagnosis-rollout.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Turn on KeyCape's sign-in diagnosis page for the openbao-admin client.
|
||||
|
||||
NK-WP-0051-T02 / KEY-WP-0041-T02. Adds one line, `signInDiagnosis: true`, to
|
||||
the existing openbao-admin registration in sso/keycape-config. The field
|
||||
contract is docs/sign-in-diagnosis.md; the image that honours the flag is
|
||||
key-cape sha256:cdec9dd7 (source 8c6c071), live since 2026-10-15.
|
||||
|
||||
Modes default to non-mutating. `inspect` reads and plans; `dry-run` sends a
|
||||
server-side dry-run patch; `apply` writes and is reserved for the attended
|
||||
operator with `--attended` and the observed uid/resourceVersion. `--operation
|
||||
remove` is the rollback: it removes only that one line. Either way the issuer
|
||||
reads the change only after `kubectl -n sso rollout restart deploy/keycape`.
|
||||
|
||||
Exact-match edit: flag absent -> add; already true -> noop; any other value,
|
||||
a missing or duplicated openbao-admin client -> fail closed. The signing key,
|
||||
every other client and every other config byte are preserved. No Secret value,
|
||||
parser message or Kubernetes error text leaves the process.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def module(name, filename):
|
||||
spec = importlib.util.spec_from_file_location(name, ROOT / filename)
|
||||
result = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(result)
|
||||
return result
|
||||
|
||||
|
||||
# Reuse the owner lane's contained kubectl, refusal type and cluster pin.
|
||||
lane = module('portal_rollout', 'portal-client-rollout.py')
|
||||
pin = lane.pin
|
||||
Refused = lane.Refused
|
||||
require = lane.require
|
||||
|
||||
CLIENT_ID = 'openbao-admin'
|
||||
FLAG = 'signInDiagnosis'
|
||||
LINE = FLAG + ': true'
|
||||
|
||||
|
||||
def _inspect(secret):
|
||||
raw, config, _ = pin.issuer_document(secret)
|
||||
require(config['authelia'].get('issuer') == pin.VERIFIED_UPSTREAM_ISSUER,
|
||||
'verified_issuer_pin_required')
|
||||
clients = config.get('clients')
|
||||
require(isinstance(clients, list) and clients, 'client_sequence_required')
|
||||
require(all(isinstance(c, dict) and 'clientId' in c for c in clients), 'client_shape_unexpected')
|
||||
ids = [c['clientId'] for c in clients]
|
||||
require(len(ids) == len(set(ids)), 'duplicate_client_id')
|
||||
require(CLIENT_ID in ids, 'openbao_client_missing')
|
||||
position = ids.index(CLIENT_ID)
|
||||
value = clients[position].get(FLAG, None)
|
||||
require(value in (None, True), 'existing_flag_differs_requires_reconciliation')
|
||||
root = yaml.compose(raw)
|
||||
sequence = next(v for k, v in root.value if k.value == 'clients')
|
||||
require(isinstance(sequence, yaml.nodes.SequenceNode) and not sequence.flow_style,
|
||||
'block_client_sequence_required')
|
||||
item = sequence.value[position]
|
||||
require(isinstance(item, yaml.nodes.MappingNode) and not item.flow_style,
|
||||
'block_client_mapping_required')
|
||||
return raw, config, position, item
|
||||
|
||||
|
||||
def _line_end(raw, index):
|
||||
end = raw.find('\n', index)
|
||||
return len(raw) if end < 0 else end + 1
|
||||
|
||||
|
||||
def enable(secret):
|
||||
"""Return (base64 config, changed) with only the flag line added."""
|
||||
raw, config, position, item = _inspect(secret)
|
||||
if config['clients'][position].get(FLAG) is True:
|
||||
return secret['data']['config.yaml'], False
|
||||
key, value = item.value[0]
|
||||
require(isinstance(value, yaml.nodes.ScalarNode)
|
||||
and key.start_mark.line == value.end_mark.line, 'unexpected_item_layout')
|
||||
index = _line_end(raw, value.end_mark.index)
|
||||
prefix = '' if raw[index - 1:index] == '\n' else '\n'
|
||||
updated = raw[:index] + prefix + ' ' * key.start_mark.column + LINE + '\n' + raw[index:]
|
||||
expected = copy.deepcopy(config)
|
||||
expected['clients'][position][FLAG] = True
|
||||
require(yaml.load(updated, Loader=pin.UniqueLoader) == expected, 'unrelated_configuration_changed')
|
||||
return base64.b64encode(updated.encode()).decode(), True
|
||||
|
||||
|
||||
def disable(secret):
|
||||
"""Rollback: return (base64 config, changed) with only the flag line removed."""
|
||||
raw, config, position, item = _inspect(secret)
|
||||
if FLAG not in config['clients'][position]:
|
||||
return secret['data']['config.yaml'], False
|
||||
key = next(k for k, _ in item.value if k.value == FLAG)
|
||||
require(key is not item.value[0][0], 'unexpected_item_layout')
|
||||
start = raw.rfind('\n', 0, key.start_mark.index) + 1
|
||||
end = _line_end(raw, key.start_mark.index)
|
||||
require(raw[start:end].strip() == LINE, 'unexpected_item_layout')
|
||||
updated = raw[:start] + raw[end:]
|
||||
expected = copy.deepcopy(config)
|
||||
del expected['clients'][position][FLAG]
|
||||
require(yaml.load(updated, Loader=pin.UniqueLoader) == expected, 'unrelated_configuration_changed')
|
||||
return base64.b64encode(updated.encode()).decode(), True
|
||||
|
||||
|
||||
def run(args, kubectl=None):
|
||||
kubectl = kubectl or lane.kubectl
|
||||
cluster = kubectl(['get', 'namespace', 'kube-system', '-o', 'json'])
|
||||
require(cluster['metadata']['uid'] == lane.CLUSTER_UID, 'cluster_identity_changed')
|
||||
before = kubectl(['-n', 'sso', 'get', 'secret', 'keycape-config', '-o', 'json'])
|
||||
require(not before['metadata'].get('ownerReferences'), 'controller_owned_secret')
|
||||
metadata = pin.safe_metadata(before)
|
||||
encoded, changed = (enable if args.operation == 'add' else disable)(before)
|
||||
receipt = {'schema': 'netkingdom.keycape-openbao-signin-diagnosis.v1',
|
||||
'target': 'sso/keycape-config', 'before': metadata, 'operation': args.operation,
|
||||
'client_id': CLIENT_ID, 'flag': FLAG, 'change_needed': changed, 'mode': args.mode,
|
||||
'values_emitted': False, 'changed': False}
|
||||
if args.mode == 'inspect':
|
||||
return receipt
|
||||
if args.mode == 'apply':
|
||||
require(args.attended, 'attended_operator_required')
|
||||
require(metadata == {'uid': args.expected_uid, 'resource_version': args.expected_resource_version},
|
||||
'observed_revision_changed')
|
||||
if changed:
|
||||
patch = [
|
||||
{'op': 'test', 'path': '/metadata/uid', 'value': metadata['uid']},
|
||||
{'op': 'test', 'path': '/metadata/resourceVersion', 'value': metadata['resource_version']},
|
||||
{'op': 'replace', 'path': '/data/config.yaml', 'value': encoded},
|
||||
]
|
||||
command = ['-n', 'sso', 'patch', 'secret', 'keycape-config', '--type=json',
|
||||
'--patch-file=/dev/stdin', '-o', 'json']
|
||||
if args.mode == 'dry-run':
|
||||
command += ['--dry-run=server']
|
||||
result = kubectl(command, patch)
|
||||
require(result['data'] == dict(before['data'], **{'config.yaml': encoded}), 'patch_response_mismatch')
|
||||
if args.mode == 'apply':
|
||||
after = kubectl(['-n', 'sso', 'get', 'secret', 'keycape-config', '-o', 'json'])
|
||||
require(after['metadata']['uid'] == metadata['uid']
|
||||
and after['data'] == dict(before['data'], **{'config.yaml': encoded}),
|
||||
'readback_mismatch_stop_without_stale_replay')
|
||||
receipt.update(changed=changed, after=pin.safe_metadata(after), restart_required=changed)
|
||||
receipt.update(unrelated_config_bytes_preserved=True, other_secret_data_unchanged=True)
|
||||
return receipt
|
||||
|
||||
|
||||
def main(argv=None, kubectl=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__.split('\n', 1)[0])
|
||||
parser.add_argument('mode', nargs='?', default='inspect', choices=['inspect', 'dry-run', 'apply'])
|
||||
parser.add_argument('--operation', default='add', choices=['add', 'remove'])
|
||||
parser.add_argument('--expected-uid')
|
||||
parser.add_argument('--expected-resource-version')
|
||||
parser.add_argument('--attended', action='store_true', help='required for apply')
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
print(json.dumps(run(args, kubectl), sort_keys=True))
|
||||
return 0
|
||||
except Refused as error:
|
||||
print(json.dumps({'status': 'refused', 'reason': str(error)}))
|
||||
except pin.IssuerPinError as error:
|
||||
print(json.dumps({'status': 'refused', 'reason': str(error)}))
|
||||
except Exception:
|
||||
# Parser and Kubernetes errors can contain full Secret material.
|
||||
print(json.dumps({'status': 'failed', 'reason': 'contained_operation_failed'}))
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
156
sso-mfa/k8s/keycape/test_openbao_signin_diagnosis_rollout.py
Normal file
156
sso-mfa/k8s/keycape/test_openbao_signin_diagnosis_rollout.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import base64
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
'openbao_signin_diagnosis_rollout', Path(__file__).with_name('openbao-signin-diagnosis-rollout.py'))
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
UID = '00000000-0000-4000-8000-000000000001'
|
||||
CLUSTER = {'metadata': {'uid': module.lane.CLUSTER_UID}}
|
||||
OPENBAO = (' - clientId: openbao-admin\n'
|
||||
' displayName: Railiance OpenBao Admin\n'
|
||||
' redirectUris:\n'
|
||||
' - http://localhost:8250/oidc/callback\n'
|
||||
' clientType: public\n')
|
||||
OTHER = (' - clientId: user-engine-portal\n'
|
||||
' clientType: public\n')
|
||||
|
||||
|
||||
def fixture(clients=OTHER + OPENBAO, tail='tokenLifetime: 15m\n'):
|
||||
# Synthetic marker values only; no live Secret is loaded by this suite.
|
||||
raw = ('# preserve this comment\nissuer: https://kc.coulomb.social\n'
|
||||
'authelia:\n issuer: https://auth.coulomb.social\n clientSecret: synthetic\n'
|
||||
'clients:\n' + clients + tail)
|
||||
secret = {'metadata': {'uid': UID, 'resourceVersion': '7'},
|
||||
'data': {'config.yaml': base64.b64encode(raw.encode()).decode(),
|
||||
'signing-key.pem': base64.b64encode(b'synthetic').decode()}}
|
||||
return secret, raw
|
||||
|
||||
|
||||
def decode(encoded):
|
||||
return base64.b64decode(encoded).decode()
|
||||
|
||||
|
||||
class EditTests(unittest.TestCase):
|
||||
def test_enable_adds_exactly_one_line_after_client_id(self):
|
||||
secret, raw = fixture()
|
||||
encoded, changed = module.enable(secret)
|
||||
self.assertTrue(changed)
|
||||
expected = raw.replace(' - clientId: openbao-admin\n',
|
||||
' - clientId: openbao-admin\n signInDiagnosis: true\n')
|
||||
self.assertEqual(decode(encoded), expected)
|
||||
|
||||
def test_enable_is_noop_when_already_true(self):
|
||||
secret, _ = fixture(OPENBAO.replace('clientType: public', 'clientType: public\n signInDiagnosis: true'))
|
||||
encoded, changed = module.enable(secret)
|
||||
self.assertFalse(changed)
|
||||
self.assertEqual(encoded, secret['data']['config.yaml'])
|
||||
|
||||
def test_other_flag_value_fails_closed(self):
|
||||
secret, _ = fixture(OPENBAO + ' signInDiagnosis: false\n')
|
||||
for operation in (module.enable, module.disable):
|
||||
with self.assertRaises(module.Refused):
|
||||
operation(secret)
|
||||
|
||||
def test_missing_or_duplicate_client_fails_closed(self):
|
||||
for clients in (OTHER, OPENBAO + OPENBAO):
|
||||
with self.assertRaises(module.Refused):
|
||||
module.enable(fixture(clients)[0])
|
||||
|
||||
def test_unverified_upstream_issuer_refused(self):
|
||||
secret, raw = fixture()
|
||||
raw = raw.replace('https://auth.coulomb.social', 'https://evil.test')
|
||||
secret['data']['config.yaml'] = base64.b64encode(raw.encode()).decode()
|
||||
with self.assertRaises(module.Refused):
|
||||
module.enable(secret)
|
||||
|
||||
def test_rollback_restores_exact_bytes(self):
|
||||
secret, raw = fixture()
|
||||
enabled, _ = module.enable(secret)
|
||||
secret['data']['config.yaml'] = enabled
|
||||
restored, changed = module.disable(secret)
|
||||
self.assertTrue(changed)
|
||||
self.assertEqual(decode(restored), raw)
|
||||
|
||||
def test_rollback_absent_is_noop(self):
|
||||
secret, _ = fixture()
|
||||
encoded, changed = module.disable(secret)
|
||||
self.assertFalse(changed)
|
||||
self.assertEqual(encoded, secret['data']['config.yaml'])
|
||||
|
||||
|
||||
class RunTests(unittest.TestCase):
|
||||
"""Drive run() against a fake kubectl; nothing reaches a cluster."""
|
||||
|
||||
def setUp(self):
|
||||
self.secret, _ = fixture()
|
||||
self.calls = []
|
||||
|
||||
def fake(self, arguments, payload=None):
|
||||
self.calls.append((arguments, payload))
|
||||
if arguments[:2] == ['get', 'namespace']:
|
||||
return CLUSTER
|
||||
if 'patch' in arguments:
|
||||
data = dict(self.secret['data'], **{'config.yaml': payload[-1]['value']})
|
||||
if '--dry-run=server' not in arguments:
|
||||
self.secret = {'metadata': {'uid': UID, 'resourceVersion': '8'}, 'data': data}
|
||||
return {'data': data}
|
||||
return self.secret
|
||||
|
||||
def main(self, *argv):
|
||||
out = io.StringIO()
|
||||
with contextlib.redirect_stdout(out):
|
||||
code = module.main(list(argv), kubectl=self.fake)
|
||||
return code, out.getvalue()
|
||||
|
||||
def patches(self):
|
||||
return [c for c in self.calls if 'patch' in c[0]]
|
||||
|
||||
def test_default_is_inspect_and_never_patches(self):
|
||||
code, out = self.main()
|
||||
self.assertEqual(code, 0)
|
||||
receipt = json.loads(out)
|
||||
self.assertTrue(receipt['change_needed'])
|
||||
self.assertEqual(receipt['before'], {'uid': UID, 'resource_version': '7'})
|
||||
self.assertEqual(self.patches(), [])
|
||||
self.assertNotIn('synthetic', out)
|
||||
|
||||
def test_dry_run_requires_observed_revision_and_is_server_side(self):
|
||||
code, out = self.main('dry-run')
|
||||
self.assertEqual((code, json.loads(out)['reason']), (1, 'observed_revision_changed'))
|
||||
code, out = self.main('dry-run', '--expected-uid', UID, '--expected-resource-version', '7')
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn('--dry-run=server', self.patches()[-1][0])
|
||||
self.assertNotIn('synthetic', out)
|
||||
|
||||
def test_apply_requires_attended_and_reports_restart(self):
|
||||
code, out = self.main('apply', '--expected-uid', UID, '--expected-resource-version', '7')
|
||||
self.assertEqual((code, json.loads(out)['reason']), (1, 'attended_operator_required'))
|
||||
self.assertEqual(self.patches(), [])
|
||||
code, out = self.main('apply', '--attended', '--expected-uid', UID, '--expected-resource-version', '7')
|
||||
receipt = json.loads(out)
|
||||
self.assertEqual(code, 0)
|
||||
self.assertTrue(receipt['changed'] and receipt['restart_required'])
|
||||
self.assertEqual(receipt['after']['resource_version'], '8')
|
||||
self.assertEqual(self.patches()[-1][1][:2], [
|
||||
{'op': 'test', 'path': '/metadata/uid', 'value': UID},
|
||||
{'op': 'test', 'path': '/metadata/resourceVersion', 'value': '7'}])
|
||||
self.assertNotIn('synthetic', out)
|
||||
|
||||
def test_failures_never_print_secret_material(self):
|
||||
def broken(arguments, payload=None):
|
||||
raise RuntimeError('synthetic value leaked in error text')
|
||||
out = io.StringIO()
|
||||
with contextlib.redirect_stdout(out):
|
||||
self.assertEqual(module.main([], kubectl=broken), 1)
|
||||
self.assertNotIn('synthetic', out.getvalue())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -113,6 +113,11 @@ current flow. The page has one action, continue, and a line that says how
|
|||
to sign in as someone else. A groups
|
||||
refusal from OpenBao then follows a screen that already named the uid.
|
||||
|
||||
The live flag is set with
|
||||
`sso-mfa/k8s/keycape/openbao-signin-diagnosis-rollout.py` (inspect, server
|
||||
dry-run, attended apply on the observed uid/resourceVersion; `--operation
|
||||
remove` is the rollback), followed by an issuer restart.
|
||||
|
||||
Acceptance is a deployed key-cape image. The log line contains no token,
|
||||
code, email, or one-time password. This repo records the image digest in
|
||||
the T01 document.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue