Record canonical portal hostname and callback cutover
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a09cbb-87c6-7900-a145-4ce53ba9f1a6
This commit is contained in:
parent
bea425cf78
commit
dc42b0344c
6 changed files with 78 additions and 6 deletions
|
|
@ -30,6 +30,8 @@
|
||||||
| 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 |
|
| workplan | NK-WP-0036 | active | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
||||||
|
| workplan | NK-WP-0037 | active | — | workplans/NK-WP-0037-vergabe-company-welcome.md |
|
||||||
|
| workplan | NK-WP-0038 | finished | — | workplans/NK-WP-0038-tenant-scoped-identity-lifecycle.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 |
|
||||||
|
|
@ -134,6 +136,11 @@
|
||||||
| task | NK-WP-0036-T03 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
| task | NK-WP-0036-T03 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
||||||
| task | NK-WP-0036-T04 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
| task | NK-WP-0036-T04 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
||||||
| task | NK-WP-0036-T05 | todo | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
| task | NK-WP-0036-T05 | todo | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
||||||
|
| task | NK-WP-0036-T06 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
||||||
|
| task | NK-WP-0037-T01 | done | — | workplans/NK-WP-0037-vergabe-company-welcome.md |
|
||||||
|
| task | NK-WP-0037-T02 | progress | — | workplans/NK-WP-0037-vergabe-company-welcome.md |
|
||||||
|
| task | NK-WP-0038-T01 | done | — | workplans/NK-WP-0038-tenant-scoped-identity-lifecycle.md |
|
||||||
|
| task | NK-WP-0038-T02 | done | — | workplans/NK-WP-0038-tenant-scoped-identity-lifecycle.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 |
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ clients:
|
||||||
displayName: User Engine Portal
|
displayName: User Engine Portal
|
||||||
redirectUris:
|
redirectUris:
|
||||||
- https://users.92-205-62-239.nip.io/oidc/callback
|
- https://users.92-205-62-239.nip.io/oidc/callback
|
||||||
|
- https://users.coulomb.social/oidc/callback
|
||||||
allowedScopes:
|
allowedScopes:
|
||||||
- openid
|
- openid
|
||||||
- profile
|
- profile
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import subprocess
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
ROOT = Path('/home/worsch/net-kingdom/sso-mfa/k8s/keycape')
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
def module(name, filename):
|
def module(name, filename):
|
||||||
|
|
@ -43,9 +43,32 @@ def replacement(secret):
|
||||||
ids = [client['clientId'] for client in clients]
|
ids = [client['clientId'] for client in clients]
|
||||||
require(len(ids) == len(set(ids)), 'duplicate_client_id')
|
require(len(ids) == len(set(ids)), 'duplicate_client_id')
|
||||||
if portal.CLIENT_ID in ids:
|
if portal.CLIENT_ID in ids:
|
||||||
require(clients[ids.index(portal.CLIENT_ID)] == portal.CLIENT,
|
current = clients[ids.index(portal.CLIENT_ID)]
|
||||||
'existing_registration_differs_requires_reconciliation')
|
if current == portal.CLIENT:
|
||||||
return secret['data']['config.yaml'], False
|
return secret['data']['config.yaml'], False
|
||||||
|
legacy = dict(portal.CLIENT, redirectUris=[
|
||||||
|
'https://users.92-205-62-239.nip.io/oidc/callback'])
|
||||||
|
require(current == legacy, 'existing_registration_differs_requires_reconciliation')
|
||||||
|
# Only the exact reviewed legacy public client may gain this callback.
|
||||||
|
root = yaml.compose(raw)
|
||||||
|
node = next(value for key, value in root.value if key.value == 'clients')
|
||||||
|
item = node.value[ids.index(portal.CLIENT_ID)]
|
||||||
|
redirects = next(value for key, value in item.value if key.value == 'redirectUris')
|
||||||
|
require(isinstance(redirects, yaml.nodes.SequenceNode) and not redirects.flow_style,
|
||||||
|
'block_redirect_sequence_required')
|
||||||
|
index = redirects.end_mark.index
|
||||||
|
line_start = raw.rfind('\n', 0, index) + 1
|
||||||
|
if not raw[line_start:index].strip():
|
||||||
|
index = line_start
|
||||||
|
addition = ' ' * redirects.start_mark.column + '- https://users.coulomb.social/oidc/callback\n'
|
||||||
|
if index and raw[index - 1] != '\n':
|
||||||
|
addition = '\n' + addition
|
||||||
|
updated = raw[:index] + addition + raw[index:]
|
||||||
|
expected = copy.deepcopy(config)
|
||||||
|
expected['clients'][ids.index(portal.CLIENT_ID)] = portal.CLIENT
|
||||||
|
require(yaml.load(updated, Loader=pin.UniqueLoader) == expected,
|
||||||
|
'unrelated_configuration_changed')
|
||||||
|
return base64.b64encode(updated.encode()).decode(), True
|
||||||
root = yaml.compose(raw)
|
root = yaml.compose(raw)
|
||||||
node = next(value for key, value in root.value if key.value == 'clients')
|
node = next(value for key, value in root.value if key.value == 'clients')
|
||||||
require(isinstance(node, yaml.nodes.SequenceNode) and not node.flow_style,
|
require(isinstance(node, yaml.nodes.SequenceNode) and not node.flow_style,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ CLIENT_ID = "user-engine-portal"
|
||||||
CLIENT = {
|
CLIENT = {
|
||||||
"clientId": CLIENT_ID,
|
"clientId": CLIENT_ID,
|
||||||
"displayName": "User Engine Portal",
|
"displayName": "User Engine Portal",
|
||||||
"redirectUris": ["https://users.92-205-62-239.nip.io/oidc/callback"],
|
"redirectUris": ["https://users.92-205-62-239.nip.io/oidc/callback",
|
||||||
|
"https://users.coulomb.social/oidc/callback"],
|
||||||
"allowedScopes": ["openid", "profile", "email", "groups"],
|
"allowedScopes": ["openid", "profile", "email", "groups"],
|
||||||
"grantTypes": ["authorization_code"],
|
"grantTypes": ["authorization_code"],
|
||||||
"clientType": "public",
|
"clientType": "public",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,25 @@ class PortalClientTests(unittest.TestCase):
|
||||||
self.assertEqual(yaml.safe_load(updated), expected)
|
self.assertEqual(yaml.safe_load(updated), expected)
|
||||||
self.assertEqual(secret['data']['key.pem'], 'synthetic-key')
|
self.assertEqual(secret['data']['key.pem'], 'synthetic-key')
|
||||||
|
|
||||||
|
def test_legacy_callback_upgrade_preserves_bytes_and_is_idempotent(self):
|
||||||
|
legacy = dict(rollout.portal.CLIENT, redirectUris=[
|
||||||
|
'https://users.92-205-62-239.nip.io/oidc/callback'])
|
||||||
|
secret, raw = self.fixture([{'clientId': 'existing', 'clientType': 'public'}, legacy])
|
||||||
|
encoded, changed = rollout.replacement(secret)
|
||||||
|
updated = base64.b64decode(encoded).decode()
|
||||||
|
self.assertTrue(changed)
|
||||||
|
self.assertEqual(updated.replace(' - https://users.coulomb.social/oidc/callback\n', ''), raw)
|
||||||
|
self.assertEqual(yaml.safe_load(updated)['clients'][-1], rollout.portal.CLIENT)
|
||||||
|
secret['data']['config.yaml'] = encoded
|
||||||
|
self.assertEqual(rollout.replacement(secret), (encoded, False))
|
||||||
|
|
||||||
|
def test_upgrade_refuses_extra_scope_or_unreviewed_callback(self):
|
||||||
|
for extra in [dict(allowedScopes=['openid', 'admin']),
|
||||||
|
dict(redirectUris=['https://users.coulomb.social/oidc/callback']),
|
||||||
|
dict(clientType='confidential')]:
|
||||||
|
with self.subTest(extra=extra), self.assertRaises(rollout.Refused):
|
||||||
|
rollout.replacement(self.fixture([dict(rollout.portal.CLIENT, **extra)])[0])
|
||||||
|
|
||||||
def test_exact_existing_registration_is_noop(self):
|
def test_exact_existing_registration_is_noop(self):
|
||||||
secret, _ = self.fixture([rollout.portal.CLIENT])
|
secret, _ = self.fixture([rollout.portal.CLIENT])
|
||||||
self.assertEqual(rollout.replacement(secret), (secret['data']['config.yaml'], False))
|
self.assertEqual(rollout.replacement(secret), (secret['data']['config.yaml'], False))
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ status: active
|
||||||
owner: the-custodian
|
owner: the-custodian
|
||||||
topic_slug: netkingdom
|
topic_slug: netkingdom
|
||||||
created: "2026-09-11"
|
created: "2026-09-11"
|
||||||
updated: "2026-09-11"
|
updated: "2026-09-12"
|
||||||
related: [KEY-WP-0007, RAPPS-WP-0014, VERGABE-WP-0019]
|
related: [KEY-WP-0007, RAPPS-WP-0014, VERGABE-WP-0019]
|
||||||
state_hub_workstream_id: "6e1358d6-87e4-52e7-b3dd-09abdc48cefc"
|
state_hub_workstream_id: "6e1358d6-87e4-52e7-b3dd-09abdc48cefc"
|
||||||
---
|
---
|
||||||
|
|
@ -155,3 +155,24 @@ lane, then verify provider login, user creation/linkage and password setup as a
|
||||||
single native onboarding journey. Preserve credential secrecy and avoid
|
single native onboarding journey. Preserve credential secrecy and avoid
|
||||||
unbounded password-check retries. Actual demo users and application admission
|
unbounded password-check retries. Actual demo users and application admission
|
||||||
remain RAPPS-WP-0014 and VERGABE-WP-0019.
|
remain RAPPS-WP-0014 and VERGABE-WP-0019.
|
||||||
|
|
||||||
|
|
||||||
|
## Admit the canonical users hostname and preserve callback validation
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: NK-WP-0036-T06
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "d736f4d1-1c97-5cf3-894e-600ea113c785"
|
||||||
|
```
|
||||||
|
|
||||||
|
Operator supplied DNS for users.coulomb.social on 2026-09-12. The contained
|
||||||
|
portal-client-rollout helper now upgrades only the exact legacy public client
|
||||||
|
by appending https://users.coulomb.social/oidc/callback. It preserves unrelated
|
||||||
|
configuration bytes and Secret fields; six synthetic tests pass, including
|
||||||
|
idempotence and refusal of altered scopes/type/callbacks. The bootstrap and
|
||||||
|
registration declarations agree. Server dry-run passed; guarded Secret revision
|
||||||
|
59999030 became 60090393, UID unchanged. KeyCape restarted at its unchanged
|
||||||
|
image/resources. Canonical authorize redirects to Authelia; wrong callback and
|
||||||
|
missing PKCE return 400. The runtime package owns portal routing and callback
|
||||||
|
cutover; final human login confirmation is recorded there. No password rotated.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue