diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 19eabe4..d433e63 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -30,6 +30,8 @@ | 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-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-T02 | done | — | workplans/ADHOC-2026-07-02.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-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-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-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | | intake | NET-IN-0001 | open | — | intakes/intakes.md | diff --git a/sso-mfa/k8s/keycape/create-secrets.sh b/sso-mfa/k8s/keycape/create-secrets.sh index 8c98cf9..3f26c3d 100644 --- a/sso-mfa/k8s/keycape/create-secrets.sh +++ b/sso-mfa/k8s/keycape/create-secrets.sh @@ -117,6 +117,7 @@ clients: displayName: User Engine Portal redirectUris: - https://users.92-205-62-239.nip.io/oidc/callback + - https://users.coulomb.social/oidc/callback allowedScopes: - openid - profile diff --git a/sso-mfa/k8s/keycape/portal-client-rollout.py b/sso-mfa/k8s/keycape/portal-client-rollout.py index 8557de9..da4ee2d 100644 --- a/sso-mfa/k8s/keycape/portal-client-rollout.py +++ b/sso-mfa/k8s/keycape/portal-client-rollout.py @@ -12,7 +12,7 @@ import subprocess import yaml -ROOT = Path('/home/worsch/net-kingdom/sso-mfa/k8s/keycape') +ROOT = Path(__file__).resolve().parent def module(name, filename): @@ -43,9 +43,32 @@ def replacement(secret): 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 + current = clients[ids.index(portal.CLIENT_ID)] + if current == portal.CLIENT: + 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) node = next(value for key, value in root.value if key.value == 'clients') require(isinstance(node, yaml.nodes.SequenceNode) and not node.flow_style, diff --git a/sso-mfa/k8s/keycape/register-user-engine-portal.py b/sso-mfa/k8s/keycape/register-user-engine-portal.py index 57d82b6..3fe6eab 100644 --- a/sso-mfa/k8s/keycape/register-user-engine-portal.py +++ b/sso-mfa/k8s/keycape/register-user-engine-portal.py @@ -17,7 +17,8 @@ CLIENT_ID = "user-engine-portal" CLIENT = { "clientId": CLIENT_ID, "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"], "grantTypes": ["authorization_code"], "clientType": "public", diff --git a/sso-mfa/k8s/keycape/test_portal_client_rollout.py b/sso-mfa/k8s/keycape/test_portal_client_rollout.py index 825d6f6..01b73a1 100644 --- a/sso-mfa/k8s/keycape/test_portal_client_rollout.py +++ b/sso-mfa/k8s/keycape/test_portal_client_rollout.py @@ -32,6 +32,25 @@ class PortalClientTests(unittest.TestCase): self.assertEqual(yaml.safe_load(updated), expected) 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): secret, _ = self.fixture([rollout.portal.CLIENT]) self.assertEqual(rollout.replacement(secret), (secret['data']['config.yaml'], False)) diff --git a/workplans/NK-WP-0036-restore-user-portal-client-registration.md b/workplans/NK-WP-0036-restore-user-portal-client-registration.md index 2c64596..8c2492f 100644 --- a/workplans/NK-WP-0036-restore-user-portal-client-registration.md +++ b/workplans/NK-WP-0036-restore-user-portal-client-registration.md @@ -8,7 +8,7 @@ status: active owner: the-custodian topic_slug: netkingdom created: "2026-09-11" -updated: "2026-09-11" +updated: "2026-09-12" related: [KEY-WP-0007, RAPPS-WP-0014, VERGABE-WP-0019] 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 unbounded password-check retries. Actual demo users and application admission 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.