fix: restore native user portal login and track tenant integration
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
5293a9bea3
commit
6cd89f0a7a
7 changed files with 397 additions and 0 deletions
|
|
@ -29,6 +29,7 @@
|
||||||
| workplan | NK-WP-0033 | active | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md |
|
| workplan | NK-WP-0033 | active | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md |
|
||||||
| workplan | NK-WP-0034 | blocked | — | workplans/NK-WP-0034-verification-that-verifies.md |
|
| workplan | NK-WP-0034 | blocked | — | workplans/NK-WP-0034-verification-that-verifies.md |
|
||||||
| workplan | NK-WP-0035 | blocked | — | workplans/NK-WP-0035-emission-cadence-security-profile.md |
|
| workplan | NK-WP-0035 | blocked | — | workplans/NK-WP-0035-emission-cadence-security-profile.md |
|
||||||
|
| workplan | NK-WP-0036 | active | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
||||||
| task | NK-WP-ADHOC-2026-07-02-T01 | done | — | workplans/ADHOC-2026-07-02.md |
|
| task | NK-WP-ADHOC-2026-07-02-T01 | done | — | workplans/ADHOC-2026-07-02.md |
|
||||||
| task | NK-WP-ADHOC-2026-07-02-T02 | done | — | workplans/ADHOC-2026-07-02.md |
|
| task | NK-WP-ADHOC-2026-07-02-T02 | done | — | workplans/ADHOC-2026-07-02.md |
|
||||||
| task | NK-WP-ADHOC-2026-08-14-T01 | done | — | workplans/ADHOC-2026-08-14.md |
|
| task | NK-WP-ADHOC-2026-08-14-T01 | done | — | workplans/ADHOC-2026-08-14.md |
|
||||||
|
|
@ -128,6 +129,9 @@
|
||||||
| task | NK-WP-0035-T03 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md |
|
| task | NK-WP-0035-T03 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md |
|
||||||
| task | NK-WP-0035-T04 | wait | — | workplans/NK-WP-0035-emission-cadence-security-profile.md |
|
| task | NK-WP-0035-T04 | wait | — | workplans/NK-WP-0035-emission-cadence-security-profile.md |
|
||||||
| task | NK-WP-0035-T05 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md |
|
| task | NK-WP-0035-T05 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md |
|
||||||
|
| task | NK-WP-0036-T01 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
||||||
|
| task | NK-WP-0036-T02 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
||||||
|
| task | NK-WP-0036-T03 | progress | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
||||||
| intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
| intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
||||||
| intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
| intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
||||||
| intake | NET-IN-0001 | open | — | intakes/intakes.md |
|
| intake | NET-IN-0001 | open | — | intakes/intakes.md |
|
||||||
|
|
|
||||||
39
docs/evidence/2026-09-11-native-portal-repair.md
Normal file
39
docs/evidence/2026-09-11-native-portal-repair.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Native portal login and tenant-onboarding repair — 2026-09-11
|
||||||
|
|
||||||
|
Owner: NK-WP-0036. Actual tenant/product admission remains RAPPS-WP-0014.
|
||||||
|
|
||||||
|
The approved public `user-engine-portal` client was absent from live KeyCape.
|
||||||
|
The bounded helper repaired only that missing registration in sso/keycape-config.
|
||||||
|
Secret UID `2e94519d-1550-41c7-9701-2efe47fe1fd3`, resourceVersion
|
||||||
|
`58747126` → `59999030`; unrelated configuration bytes and Secret data preserved.
|
||||||
|
The full bootstrap generator was not run. Its source now retains the existing
|
||||||
|
client declaration; four synthetic helper/bootstrap tests pass.
|
||||||
|
|
||||||
|
KeyCape rollout succeeded with its prior image
|
||||||
|
`sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611`
|
||||||
|
and 25m CPU request. Read-only verification uses
|
||||||
|
`sso-mfa/k8s/keycape/verify-user-engine-portal.py`: valid S256 authorize redirects
|
||||||
|
to Authelia; wrong callback is refused with redirect_uri, missing PKCE with
|
||||||
|
missing_pkce. The first probe incorrectly expected code_challenge for the latter;
|
||||||
|
the probe expectation was corrected to the implementation's documented error.
|
||||||
|
|
||||||
|
The operator then authenticated natively as platform-root and reached the tenant
|
||||||
|
creation form (browser observation at 18:35:42 UTC). Their manual submission
|
||||||
|
returned provisioning_unavailable, correlation
|
||||||
|
`corr_d41f417c61713afc7ddea08f`. Tenant Engine logged POST /tenants 403 before its
|
||||||
|
store-create call. That establishes a downstream denial, not an identity-provider
|
||||||
|
outage. No tenant existence is claimed without native readback.
|
||||||
|
|
||||||
|
The deployed portal calls Tenant Engine as user-engine. Its PDP policy had no
|
||||||
|
such subject and also lacked the tenant.read action now required by the endpoint.
|
||||||
|
NK-WP-0036-T03 tracks the minimal policy integration, adapter error mapping,
|
||||||
|
immutable CI image promotion and live positive/negative verification. Human
|
||||||
|
credentials, session cookies and the chosen administrator's private contact
|
||||||
|
information are excluded from this record.
|
||||||
|
|
||||||
|
Local validation: Flex Auth make test (including race tests), 27 policy tests and
|
||||||
|
35 fixtures pass; actual registry-enriched create request allows policy v2.
|
||||||
|
User Engine make test: 169 tests, 3 optional integration skips, layer check passed;
|
||||||
|
regression proves authority 403 reaches the browser as redacted 403 without
|
||||||
|
creating an administrator. Source fixes: flex-auth dd8dd51, user-engine 3c85e56.
|
||||||
|
CI publication/live promotion are pending; these are not live success claims.
|
||||||
|
|
@ -113,6 +113,18 @@ clients:
|
||||||
allowedScopes: ["openid", "profile", "email", "groups"]
|
allowedScopes: ["openid", "profile", "email", "groups"]
|
||||||
grantTypes: ["authorization_code"]
|
grantTypes: ["authorization_code"]
|
||||||
clientType: "public"
|
clientType: "public"
|
||||||
|
- clientId: user-engine-portal
|
||||||
|
displayName: User Engine Portal
|
||||||
|
redirectUris:
|
||||||
|
- https://users.92-205-62-239.nip.io/oidc/callback
|
||||||
|
allowedScopes:
|
||||||
|
- openid
|
||||||
|
- profile
|
||||||
|
- email
|
||||||
|
- groups
|
||||||
|
grantTypes:
|
||||||
|
- authorization_code
|
||||||
|
clientType: public
|
||||||
- clientId: "netkingdom-bootstrap-console"
|
- clientId: "netkingdom-bootstrap-console"
|
||||||
displayName: "NetKingdom Bootstrap Console"
|
displayName: "NetKingdom Bootstrap Console"
|
||||||
redirectUris:
|
redirectUris:
|
||||||
|
|
|
||||||
136
sso-mfa/k8s/keycape/portal-client-rollout.py
Normal file
136
sso-mfa/k8s/keycape/portal-client-rollout.py
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Restore the approved public portal client without exporting Secret values."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import copy
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
ROOT = Path('/home/worsch/net-kingdom/sso-mfa/k8s/keycape')
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
pin = module('issuer_pin', 'openbao-client-config.py')
|
||||||
|
portal = module('portal_registration', 'register-user-engine-portal.py')
|
||||||
|
CLUSTER_UID = 'a553c742-0115-43d4-99a4-a5ca56fe0786'
|
||||||
|
|
||||||
|
|
||||||
|
class Refused(Exception):
|
||||||
|
"""Only fixed reason codes may leave the operation."""
|
||||||
|
|
||||||
|
|
||||||
|
def require(condition, reason):
|
||||||
|
if not condition:
|
||||||
|
raise Refused(reason)
|
||||||
|
|
||||||
|
|
||||||
|
def replacement(secret):
|
||||||
|
raw, config, _ = pin.issuer_document(secret)
|
||||||
|
clients = config.get('clients')
|
||||||
|
require(isinstance(clients, list) and clients, 'client_sequence_required')
|
||||||
|
ids = [client['clientId'] for client in clients]
|
||||||
|
require(len(ids) == len(set(ids)), 'duplicate_client_id')
|
||||||
|
if portal.CLIENT_ID in ids:
|
||||||
|
require(clients[ids.index(portal.CLIENT_ID)] == portal.CLIENT,
|
||||||
|
'existing_registration_differs_requires_reconciliation')
|
||||||
|
return secret['data']['config.yaml'], False
|
||||||
|
root = yaml.compose(raw)
|
||||||
|
node = next(value for key, value in root.value if key.value == 'clients')
|
||||||
|
require(isinstance(node, yaml.nodes.SequenceNode) and not node.flow_style,
|
||||||
|
'block_client_sequence_required')
|
||||||
|
index = node.end_mark.index
|
||||||
|
line_start = raw.rfind('\n', 0, index) + 1
|
||||||
|
if not raw[line_start:index].strip():
|
||||||
|
index = line_start
|
||||||
|
indent = ' ' * node.start_mark.column
|
||||||
|
addition = yaml.safe_dump([portal.CLIENT], sort_keys=False)
|
||||||
|
addition = ''.join(indent + line if line.strip() else line
|
||||||
|
for line in addition.splitlines(True))
|
||||||
|
if index and raw[index - 1] != '\n':
|
||||||
|
addition = '\n' + addition
|
||||||
|
updated = raw[:index] + addition + raw[index:]
|
||||||
|
expected = copy.deepcopy(config)
|
||||||
|
expected['clients'].append(portal.CLIENT)
|
||||||
|
require(yaml.load(updated, Loader=pin.UniqueLoader) == expected,
|
||||||
|
'unrelated_configuration_changed')
|
||||||
|
return base64.b64encode(updated.encode()).decode(), True
|
||||||
|
|
||||||
|
|
||||||
|
def kubectl(arguments, payload=None):
|
||||||
|
args = ['kubectl', '--kubeconfig', '/home/worsch/.kube/config-hosteurope',
|
||||||
|
'--server', 'https://127.0.0.1:16444', '--request-timeout=20s', *arguments]
|
||||||
|
result = subprocess.run(args, input=None if payload is None else json.dumps(payload),
|
||||||
|
text=True, capture_output=True, timeout=30)
|
||||||
|
require(result.returncode == 0, 'kubernetes_operation_failed')
|
||||||
|
return json.loads(result.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def run(args):
|
||||||
|
cluster = kubectl(['get', 'namespace', 'kube-system', '-o', 'json'])
|
||||||
|
require(cluster['metadata']['uid'] == 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 = replacement(before)
|
||||||
|
receipt = {'target': 'sso/keycape-config', 'before': metadata,
|
||||||
|
'client': portal.CLIENT, 'change_needed': changed,
|
||||||
|
'mode': args.mode, 'values_emitted': False, 'changed': False}
|
||||||
|
if args.mode == 'inspect':
|
||||||
|
return receipt
|
||||||
|
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))
|
||||||
|
receipt.update(unrelated_config_bytes_preserved=True, other_secret_data_unchanged=True)
|
||||||
|
return receipt
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('mode', choices=['inspect', 'dry-run', 'apply'])
|
||||||
|
parser.add_argument('--expected-uid')
|
||||||
|
parser.add_argument('--expected-resource-version')
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
print(json.dumps(run(args), sort_keys=True))
|
||||||
|
return 0
|
||||||
|
except Refused 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())
|
||||||
54
sso-mfa/k8s/keycape/test_portal_client_rollout.py
Normal file
54
sso-mfa/k8s/keycape/test_portal_client_rollout.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import base64
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location('portal_rollout', Path(__file__).with_name('portal-client-rollout.py'))
|
||||||
|
rollout = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(rollout)
|
||||||
|
|
||||||
|
|
||||||
|
class PortalClientTests(unittest.TestCase):
|
||||||
|
def fixture(self, clients=None):
|
||||||
|
# Synthetic marker values only; no live Secret is loaded by this suite.
|
||||||
|
raw = ('# preserve this comment\nauthelia:\n issuer: https://auth.example.invalid\n'
|
||||||
|
' clientSecret: synthetic-marker\nclients:\n' +
|
||||||
|
yaml.safe_dump(json.loads(json.dumps(clients or [{'clientId': 'existing', 'clientType': 'public'}])), sort_keys=False) +
|
||||||
|
'tokenLifetime: 15m\n')
|
||||||
|
return {'data': {'config.yaml': base64.b64encode(raw.encode()).decode(), 'key.pem': 'synthetic-key'}}, raw
|
||||||
|
|
||||||
|
def test_add_preserves_other_values_and_original_bytes(self):
|
||||||
|
secret, raw = self.fixture()
|
||||||
|
encoded, changed = rollout.replacement(secret)
|
||||||
|
updated = base64.b64decode(encoded).decode()
|
||||||
|
self.assertTrue(changed)
|
||||||
|
self.assertTrue(updated.startswith(raw[:raw.index('tokenLifetime:')]))
|
||||||
|
self.assertTrue(updated.endswith('tokenLifetime: 15m\n'))
|
||||||
|
expected = yaml.safe_load(raw)
|
||||||
|
expected['clients'].append(rollout.portal.CLIENT)
|
||||||
|
self.assertEqual(yaml.safe_load(updated), expected)
|
||||||
|
self.assertEqual(secret['data']['key.pem'], 'synthetic-key')
|
||||||
|
|
||||||
|
def test_exact_existing_registration_is_noop(self):
|
||||||
|
secret, _ = self.fixture([rollout.portal.CLIENT])
|
||||||
|
self.assertEqual(rollout.replacement(secret), (secret['data']['config.yaml'], False))
|
||||||
|
|
||||||
|
def test_differing_or_duplicate_registration_refused(self):
|
||||||
|
for clients in [[dict(rollout.portal.CLIENT, redirectUris=['https://other.invalid/callback'])],
|
||||||
|
[rollout.portal.CLIENT, rollout.portal.CLIENT]]:
|
||||||
|
with self.subTest(clients=len(clients)), self.assertRaises(rollout.Refused):
|
||||||
|
rollout.replacement(self.fixture(clients)[0])
|
||||||
|
|
||||||
|
def test_bootstrap_retains_same_public_registration(self):
|
||||||
|
source = Path(__file__).with_name('create-secrets.sh').read_text()
|
||||||
|
raw = source.split('CONFIG_YAML=$(cat <<EOF\n', 1)[1].split('\nEOF', 1)[0]
|
||||||
|
clients = yaml.safe_load(raw)['clients']
|
||||||
|
self.assertEqual([c for c in clients if c['clientId'] == rollout.portal.CLIENT_ID],
|
||||||
|
[rollout.portal.CLIENT])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
56
sso-mfa/k8s/keycape/verify-user-engine-portal.py
Normal file
56
sso-mfa/k8s/keycape/verify-user-engine-portal.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Read-only native authorize checks; print no state, cookies or auth tokens."""
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import secrets
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
from urllib.parse import urlencode, urlsplit
|
||||||
|
from urllib.request import HTTPRedirectHandler, build_opener
|
||||||
|
|
||||||
|
|
||||||
|
class NoRedirect(HTTPRedirectHandler):
|
||||||
|
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def verify():
|
||||||
|
opener = build_opener(NoRedirect)
|
||||||
|
base = {
|
||||||
|
"client_id": "user-engine-portal", "response_type": "code",
|
||||||
|
"redirect_uri": "https://users.92-205-62-239.nip.io/oidc/callback",
|
||||||
|
"scope": "openid profile email groups", "state": secrets.token_urlsafe(24),
|
||||||
|
"code_challenge": base64.urlsafe_b64encode(
|
||||||
|
hashlib.sha256(secrets.token_bytes(32)).digest()
|
||||||
|
).rstrip(b"=").decode(), "code_challenge_method": "S256",
|
||||||
|
}
|
||||||
|
results = []
|
||||||
|
for name in ("valid", "wrong_callback", "missing_pkce"):
|
||||||
|
params = dict(base)
|
||||||
|
if name == "wrong_callback":
|
||||||
|
params["redirect_uri"] = "https://invalid.example/oidc/callback"
|
||||||
|
if name == "missing_pkce":
|
||||||
|
params.pop("code_challenge")
|
||||||
|
params.pop("code_challenge_method")
|
||||||
|
try:
|
||||||
|
response = opener.open("https://kc.coulomb.social/authorize?" + urlencode(params), timeout=20)
|
||||||
|
except HTTPError as error:
|
||||||
|
response = error
|
||||||
|
with response:
|
||||||
|
row = {"check": name, "status": response.code}
|
||||||
|
if name == "valid":
|
||||||
|
location = urlsplit(response.headers.get("Location", ""))
|
||||||
|
row["redirect_origin"] = location.scheme + "://" + location.netloc
|
||||||
|
row["passed"] = response.code == 302 and row["redirect_origin"] == "https://auth.coulomb.social"
|
||||||
|
else:
|
||||||
|
body = json.loads(response.read(4096))
|
||||||
|
row.update(error=body.get("error"), feature=body.get("feature"))
|
||||||
|
expected = "redirect_uri" if name == "wrong_callback" else "missing_pkce"
|
||||||
|
row["passed"] = response.code == 400 and row["error"] == "invalid_profile_usage" and row["feature"] == expected
|
||||||
|
results.append(row)
|
||||||
|
print(json.dumps(results, indent=2))
|
||||||
|
return all(row["passed"] for row in results)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(0 if verify() else 1)
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
---
|
||||||
|
id: NK-WP-0036
|
||||||
|
type: workplan
|
||||||
|
title: "Restore native portal login and tenant-onboarding integration"
|
||||||
|
domain: infotech
|
||||||
|
repo: net-kingdom
|
||||||
|
status: active
|
||||||
|
owner: the-custodian
|
||||||
|
topic_slug: netkingdom
|
||||||
|
created: "2026-09-11"
|
||||||
|
updated: "2026-09-11"
|
||||||
|
related: [KEY-WP-0007, RAPPS-WP-0014, VERGABE-WP-0019]
|
||||||
|
state_hub_workstream_id: "6e1358d6-87e4-52e7-b3dd-09abdc48cefc"
|
||||||
|
---
|
||||||
|
|
||||||
|
The demo-company operator login returns `invalid_profile_usage`, feature
|
||||||
|
`client_id`, description `unknown client_id`. The approved public client from
|
||||||
|
KEY-WP-0007 is absent from live `sso/keycape-config`. The bootstrap generator
|
||||||
|
also omits it; this is a demonstrated regeneration hazard, not proof of which
|
||||||
|
historical change removed the live registration. User requests retry/repair
|
||||||
|
under continued demo-tenant onboarding authority.
|
||||||
|
|
||||||
|
## Preserve the existing registration in source and prepare the bounded repair
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: NK-WP-0036-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "7f30efcb-beac-5843-bca7-a5cbc1c715cc"
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep `user-engine-portal` public, authorization-code/S256 PKCE, exact callback
|
||||||
|
`https://users.92-205-62-239.nip.io/oidc/callback`, and only
|
||||||
|
`openid profile email groups`. Add the existing declaration to the bootstrap
|
||||||
|
generator and test equality with `register-user-engine-portal.py`.
|
||||||
|
`portal-client-rollout.py` adds only a missing declaration, preserving other
|
||||||
|
configuration bytes and Secret data. It refuses changed/duplicate registrations,
|
||||||
|
controller ownership, wrong cluster and stale UID/resourceVersion. Secret values
|
||||||
|
stay in memory/child stdin and never enter output, arguments or persisted files.
|
||||||
|
Four synthetic regression tests pass. No credentials or roles are granted.
|
||||||
|
|
||||||
|
## Restore and verify the live login entry point
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: NK-WP-0036-T02
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "8a3c3e91-d604-59ee-9d15-f79223667780"
|
||||||
|
```
|
||||||
|
|
||||||
|
Inspect and server-dry-run the exact observed Secret revision, apply the bounded
|
||||||
|
patch, read back unchanged unrelated data, and restart the existing KeyCape
|
||||||
|
deployment to load configuration. Keep the current image and 25m resources.
|
||||||
|
Record readiness and valid-client authorize redirect, invalid-callback and
|
||||||
|
missing-PKCE refusal. Reopen a fresh portal login only after positive preflight.
|
||||||
|
Actual human login and demo tenant creation continue in RAPPS-WP-0014-T02;
|
||||||
|
this repair alone is not evidence of tenant existence or operator entitlement.
|
||||||
|
|
||||||
|
If native verification fails, retain the exact non-secret failure and reconcile
|
||||||
|
the current resource revision; do not replay a stale complete Secret. Hand any
|
||||||
|
remaining actionable defects to live records before finishing this plan.
|
||||||
|
|
||||||
|
Live repair succeeded on 2026-09-11. The exact missing client was added without
|
||||||
|
changing unrelated Secret data. KeyCape restarted at its existing image and
|
||||||
|
resources. Valid authorization redirects to Authelia; wrong callback and missing
|
||||||
|
PKCE are refused. The operator subsequently authenticated as platform-root and
|
||||||
|
reached the platform form. See docs/evidence/2026-09-11-native-portal-repair.md.
|
||||||
|
|
||||||
|
## Repair and verify the portal-to-tenant policy contract
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: NK-WP-0036-T03
|
||||||
|
status: progress
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "378b03b3-aedc-50c3-8de7-94064794582b"
|
||||||
|
```
|
||||||
|
|
||||||
|
The real form submission returned provisioning_unavailable, correlation
|
||||||
|
corr_d41f417c61713afc7ddea08f. Tenant Engine logs show POST /tenants returning
|
||||||
|
403 before its create transaction. Its PDP recognizes only tenant-engine and
|
||||||
|
flex-auth; the shipped portal calls as user-engine. Tenant read is also missing
|
||||||
|
from that policy despite the shipped endpoint requiring it.
|
||||||
|
|
||||||
|
Register the existing user-engine service for only non-platform tenant create
|
||||||
|
and read in flex-auth/examples/tenant-engine. Keep ServiceAccount caller auth
|
||||||
|
enforced. Preserve denied grants, lifecycle, plan and guardrail mutations.
|
||||||
|
In user-engine, preserve display name and correlation id in the create request,
|
||||||
|
and map authority denial to the existing redacted 403 path, proving that no
|
||||||
|
administrator is created after a tenant-authority denial.
|
||||||
|
|
||||||
|
Run owner regression suites, publish through existing Forgejo image CI, pin and
|
||||||
|
roll only the Tenant Engine PDP and rapp-user-engine runtime, and verify the
|
||||||
|
actual allowed/denied contract with authenticated consumer calls. Then reconcile
|
||||||
|
native demo tenant existence before retrying the operator form. RAPPS-WP-0014
|
||||||
|
retains actual tenant onboarding and product placement; this workplan does not
|
||||||
|
claim tenant existence or application readiness from component tests alone.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue