diff --git a/.claude/rules/credential-routing.md b/.claude/rules/credential-routing.md deleted file mode 100644 index b2f4e80..0000000 --- a/.claude/rules/credential-routing.md +++ /dev/null @@ -1,50 +0,0 @@ -# Credential and access routing - -**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect** -for inference. Run this check **before** requesting secrets, API keys, SSH access, -login tokens, or database passwords — in any repo, not only `ops-warden`. - -ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every -other credential need belongs to another subsystem. **Do not** message -`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key. - -### Lookup (do this first) - -```bash -warden route find "" --json -warden route show --json -``` - -Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`). - -| Agent runtime | How to orient | -| --- | --- | -| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=key-cape` is for coordination, not secret vending | -| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership | -| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` | - -### Quick routing table - -| I need… | Owner | ops-warden executes? | -| --- | --- | --- | -| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes** — `warden sign` | -| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only | -| Login / OIDC / MFA | key-cape / Keycloak | No — route only | -| Authorization decision | flex-auth | No — route only | -| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` | -| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only | - -### Anti-patterns (do not do these) - -- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc. -- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist -- Pasting secrets into Git, State Hub, workplans, logs, or chat - -### Other capabilities (reuse-surface) - -Non-credential capabilities are usually discovered through **reuse-surface** federation -(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in -every repo's agent instructions because it is high-frequency, high-risk, and easy to -get wrong. - -**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml` \ No newline at end of file diff --git a/.claude/rules/first-session.md b/.claude/rules/first-session.md index 80aeac7..58f2cc7 100644 --- a/.claude/rules/first-session.md +++ b/.claude/rules/first-session.md @@ -1,11 +1,11 @@ ## First Session Protocol -Triggered when `get_domain_summary("infotech")` shows **no workstreams**. +Triggered when `get_domain_summary("netkingdom")` shows **no workstreams**. The project is registered but work has not yet been structured. **Step 1 — Read, don't write** -- `~/the-custodian/canon/projects/infotech/project_charter_v0.1.md` — purpose, scope -- `~/the-custodian/canon/projects/infotech/roadmap_v0.1.md` — planned phases +- `~/the-custodian/canon/projects/netkingdom/project_charter_v0.1.md` — purpose, scope +- `~/the-custodian/canon/projects/netkingdom/roadmap_v0.1.md` — planned phases - Scan repo root: README, directory structure, existing code or docs **Step 2 — Survey in-progress work** @@ -17,20 +17,20 @@ roadmap phase. **Wait for approval before creating.** **Step 4 — Create workplan file first, then DB record (ADR-001)** ``` -workplans/KEY-WP-NNNN-.md ← write this first +workplans/key-cape-WP-NNNN-.md ← write this first ``` Then register in the hub: ``` -create_workstream(topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", title="...", owner="...", description="...") +create_workstream(topic_id="a6c6e745-bf54-4465-9340-1534a2be493e", title="...", owner="...", description="...") create_task(workstream_id="", title="...", priority="high|medium|low") ``` **Step 5 — Record the setup** ``` add_progress_event( - summary="First session: structured infotech into N workstreams, M tasks", + summary="First session: structured netkingdom into N workstreams, M tasks", event_type="milestone", - topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", + topic_id="a6c6e745-bf54-4465-9340-1534a2be493e", detail={"workstreams": [...], "tasks_created": M} ) ``` diff --git a/.claude/rules/repo-identity.md b/.claude/rules/repo-identity.md index c4b8fa1..55343f4 100644 --- a/.claude/rules/repo-identity.md +++ b/.claude/rules/repo-identity.md @@ -1,5 +1,5 @@ **Purpose:** Lightweight IAM profile implementation for NetKingdom — "prepare for Keycloak without Keycloak". Implements the NetKingdom IAM Profile (OIDC/PKCE) via Authelia + LLDAP + privacyIDEA, with migration path to Keycloak in expanded mode. -**Domain:** infotech +**Domain:** netkingdom **Repo slug:** key-cape -**Topic ID:** cee7bedf-2b48-46ef-8601-006474f2ad7a +**Topic ID:** a6c6e745-bf54-4465-9340-1534a2be493e diff --git a/.claude/rules/session-protocol.md b/.claude/rules/session-protocol.md index 381d29c..c80ea41 100644 --- a/.claude/rules/session-protocol.md +++ b/.claude/rules/session-protocol.md @@ -1,7 +1,6 @@ ## Session Protocol -Dev Hub (State Hub API): http://127.0.0.1:8000 -MCP server name in `~/.claude.json`: `dev-hub` +State Hub: http://127.0.0.1:8000 **Step 1 — Orient** @@ -11,7 +10,7 @@ cat .custodian-brief.md ``` Then call the MCP tool for richer cross-domain context when MCP tools are exposed: ``` -get_domain_summary("infotech") +get_domain_summary("netkingdom") ``` If MCP tools are unavailable in the current agent session, use the REST API: ```bash @@ -40,11 +39,11 @@ curl -s -X PATCH "http://127.0.0.1:8000/messages//read" \ ls workplans/ ``` For each file with `status: ready`, `active`, or `blocked`, note pending -`wait`/`todo`/`progress` tasks. +`todo`/`in_progress` tasks. **Step 4 — Present brief** -1. **Active workstreams** for `infotech` — title, task counts, blocking decisions +1. **Active workstreams** for `netkingdom` — title, task counts, blocking decisions 2. **Pending tasks** from `workplans/` + any `[repo:key-cape]` hub tasks 3. **Goal guidance** — if `goal_guidance` in summary: - `needs_workplan`: surface as top action — *"Repo goal '{title}' has no workplan yet"* @@ -62,13 +61,13 @@ If no workstreams: follow First Session Protocol (`first-session.md`). **Session close:** With MCP tools: ``` -add_progress_event(summary="...", topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", workstream_id="") +add_progress_event(summary="...", topic_id="a6c6e745-bf54-4465-9340-1534a2be493e", workstream_id="") ``` Without MCP tools: ```bash curl -s -X POST http://127.0.0.1:8000/progress/ \ -H "Content-Type: application/json" \ - -d '{"topic_id":"cee7bedf-2b48-46ef-8601-006474f2ad7a","workstream_id":"","event_type":"note","summary":"what changed","author":"codex"}' + -d '{"topic_id":"a6c6e745-bf54-4465-9340-1534a2be493e","workstream_id":"","event_type":"note","summary":"what changed","author":"codex"}' ``` If workplan files were modified, ensure the local copy is up to date first: ```bash diff --git a/.claude/rules/workplan-convention.md b/.claude/rules/workplan-convention.md index aacacf4..9e3608c 100644 --- a/.claude/rules/workplan-convention.md +++ b/.claude/rules/workplan-convention.md @@ -1,7 +1,7 @@ ## Workplan Convention (ADR-001) -File location: `workplans/KEY-WP-NNNN-.md` -ID prefix: `KEY-WP-` +File location: `workplans/key-cape-WP-NNNN-.md` +ID prefix: `KEY-WP` Work items originate as files in this repo **before** being registered in the hub. @@ -12,7 +12,7 @@ repo state, and `finished` when implementation is complete. `stalled` and `needs_review` are derived health labels, not stored statuses. Closed workplans may be moved to `workplans/archived/` with a completion-date -prefix: `YYMMDD-KEY-WP-NNNN-.md`. The frontmatter id remains +prefix: `YYMMDD-key-cape-WP-NNNN-.md`. The frontmatter id remains unchanged; the prefix is only for quick visual reference. Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**: @@ -25,16 +25,4 @@ Ecosystem todos from other agents arrive as `[repo:key-cape]` hub tasks — visible at session start. Pick one up by creating the workplan file, then registering the workstream. -Task blocks use this shape: - -```task -id: KEY-WP-NNNN-T01 -status: wait | todo | progress | done | cancel -priority: high | medium | low -state_hub_task_id: "" # written by fix-consistency — do not edit -``` - -Status progression is `todo` → `progress` → `done`; use `wait` for waiting or -blocked work and `cancel` for stopped work. - diff --git a/.custodian-brief.md b/.custodian-brief.md index f467281..26011a0 100644 --- a/.custodian-brief.md +++ b/.custodian-brief.md @@ -1,8 +1,8 @@ # Custodian Brief — key-cape -**Domain:** communication -**Last synced:** 2026-06-22 16:02 UTC +**Domain:** (unknown) +**Last synced:** 2026-03-26 16:47 UTC **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* ## Active Workstreams @@ -13,6 +13,6 @@ ## MCP Orientation (when available) If the state-hub MCP server is reachable, call: -`get_domain_summary("communication")` +`get_domain_summary("")` This provides richer cross-domain context. If the MCP call fails, use this file as your orientation source. diff --git a/.repo-classification.yaml b/.repo-classification.yaml deleted file mode 100644 index 4fc521f..0000000 --- a/.repo-classification.yaml +++ /dev/null @@ -1,26 +0,0 @@ -repo_classification: - standard: Repo Classification Standard - version: '1.0' - classified_at: '2026-06-22' - classified_by: human - category: product - domain: infotech - secondary_domains: - - communication - capability_tags: - - identity - - access-control - - security - - platform - - operations - business_stake: - - technology - - operations - - legal - - product - business_mechanics: - - control - - operation - - adaptation - notes: NetKingdom IAM Profile lightweight mode (Authelia/LLDAP/privacyIDEA); human - corrected domain from communication→infotech. diff --git a/AGENTS.md b/AGENTS.md index 823d45c..cb416cc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,9 +4,9 @@ **Purpose:** Lightweight IAM profile implementation for NetKingdom — "prepare for Keycloak without Keycloak". Implements the NetKingdom IAM Profile (OIDC/PKCE) via Authelia + LLDAP + privacyIDEA, with migration path to Keycloak in expanded mode. -**Domain:** infotech +**Domain:** netkingdom **Repo slug:** key-cape -**Topic ID:** `cee7bedf-2b48-46ef-8601-006474f2ad7a` +**Topic ID:** `a6c6e745-bf54-4465-9340-1534a2be493e` **Workplan prefix:** `KEY-WP-` --- @@ -28,7 +28,7 @@ there is no MCP server for Codex agents. cat .custodian-brief.md # Active workstreams for this domain -curl -s "http://127.0.0.1:8000/workstreams/?topic_id=cee7bedf-2b48-46ef-8601-006474f2ad7a&status=active" \ +curl -s "http://127.0.0.1:8000/workstreams/?topic_id=a6c6e745-bf54-4465-9340-1534a2be493e&status=active" \ | python3 -m json.tool # Check inbox @@ -63,8 +63,8 @@ Omit `workstream_id` / `task_id` when not applicable. ```bash curl -s -X PATCH "http://127.0.0.1:8000/tasks/" \ -H "Content-Type: application/json" \ - -d '{"status": "progress"}' -# values: wait | todo | progress | done | cancel + -d '{"status": "in_progress"}' +# values: todo | in_progress | done | blocked ``` ### Flag a task for human review @@ -83,7 +83,7 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/" \ 1. `cat .custodian-brief.md` — domain goal and open workstreams (offline-safe) 2. Check inbox: `GET /messages/?to_agent=key-cape&unread_only=true`; mark read 3. Scan workplans: `ls workplans/` — note `status: ready`, `active`, or `blocked` files and open tasks -4. Check human-needed tasks: `GET /tasks/?needs_human=true` +4. Check blocked tasks: `GET /tasks/?needs_human=true` **During work:** - Update task statuses in workplan files as tasks progress @@ -101,63 +101,6 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/" \ --- -## Credential and access routing - -**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect** -for inference. Run this check **before** requesting secrets, API keys, SSH access, -login tokens, or database passwords — in any repo, not only `ops-warden`. - -ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every -other credential need belongs to another subsystem. **Do not** message -`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key. - -### Lookup (do this first) - -```bash -warden route find "" --json -warden route show --json -``` - -Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`). - -| Agent runtime | How to orient | -| --- | --- | -| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=key-cape` is for coordination, not secret vending | -| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership | -| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` | - -### Quick routing table - -| I need… | Owner | ops-warden executes? | -| --- | --- | --- | -| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes** — `warden sign` | -| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only | -| Login / OIDC / MFA | key-cape / Keycloak | No — route only | -| Authorization decision | flex-auth | No — route only | -| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` | -| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only | - -### Anti-patterns (do not do these) - -- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc. -- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist -- Pasting secrets into Git, State Hub, workplans, logs, or chat - -### Other capabilities (reuse-surface) - -Non-credential capabilities are usually discovered through **reuse-surface** federation -(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in -every repo's agent instructions because it is high-frequency, high-risk, and easy to -get wrong. - -**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml` - - - - ---- - ## Workplan Convention (ADR-001) Work items originate as files in this repo — not in the hub. The hub is a @@ -181,7 +124,7 @@ anything needing analysis, design, approval, dependencies, or multiple phases. id: KEY-WP-NNNN type: workplan title: "..." -domain: infotech +domain: netkingdom repo: key-cape status: proposed | ready | active | blocked | backlog | finished | archived owner: codex @@ -203,7 +146,7 @@ derived health labels, not frontmatter statuses. ` ` `task id: KEY-WP-NNNN-T01 -status: wait | todo | progress | done | cancel +status: todo | in_progress | done | blocked priority: high | medium | low state_hub_task_id: "" # written by fix-consistency — do not edit ` ` ` @@ -211,7 +154,7 @@ state_hub_task_id: "" # written by fix-consistency — do not edit Task description text. ``` -Status progression: `todo` → `progress` → `done`; use `wait` for waiting/blocked work and `cancel` for stopped work. +Status progression: `todo` → `in_progress` → `done` (or `blocked`) To create a new workplan: 1. Write the file following the format above diff --git a/CLAUDE.md b/CLAUDE.md index e81c1b4..759e715 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,5 +8,4 @@ @.claude/rules/stack-and-commands.md @.claude/rules/architecture.md @.claude/rules/repo-boundary.md -@.claude/rules/credential-routing.md @.claude/rules/agents.md diff --git a/registry/README.md b/registry/README.md deleted file mode 100644 index 569abe9..0000000 --- a/registry/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Capability Registry - -Markdown-first capability index for federation and reuse planning. - -## Authoring - -1. Copy a capability entry template (see reuse-surface `templates/capability-entry.template.md`). -2. Add the row to `indexes/capabilities.yaml`. -3. Run `reuse-surface validate` from a checkout with the CLI installed. -4. Merge to `main` and verify publish with `reuse-surface establish --publish-check`. - -Federation contract: reuse-surface `docs/RegistryFederation.md`. diff --git a/registry/capabilities/.gitkeep b/registry/capabilities/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/registry/indexes/capabilities.yaml b/registry/indexes/capabilities.yaml deleted file mode 100644 index f944e47..0000000 --- a/registry/indexes/capabilities.yaml +++ /dev/null @@ -1,4 +0,0 @@ -version: 1 -updated: '2026-06-16' -domain: helix_forge -capabilities: [] diff --git a/src/internal/adapters/lldap/adapter.go b/src/internal/adapters/lldap/adapter.go index 12c3c93..e929564 100644 --- a/src/internal/adapters/lldap/adapter.go +++ b/src/internal/adapters/lldap/adapter.go @@ -125,16 +125,11 @@ func (a *LDAPAdapter) LookupUser(ctx context.Context, username string) (*domain. entry := result.Entries[0] user := mapEntryToUser(entry) - // Runtime login should not fail because a live directory entry is missing - // provisioning metadata such as cn/sn. Keep the warning visible for - // diagnostics, but return the resolved user so token issuance can proceed. + // Run the canonical LDAP schema validator. snap := validator.Snapshot{Users: []domain.User{user}} report := validator.Validate(snap, validator.ModeProvisioning) if !report.Passed { - if user.LDAPAttributes == nil { - user.LDAPAttributes = make(map[string]string) - } - user.LDAPAttributes["_validation_warning"] = validationSummary(report) + return nil, fmt.Errorf("lldap: validation failed for user %q: %s", username, validationSummary(report)) } return &user, nil diff --git a/src/internal/adapters/lldap/adapter_test.go b/src/internal/adapters/lldap/adapter_test.go index 72c5bb3..b556af4 100644 --- a/src/internal/adapters/lldap/adapter_test.go +++ b/src/internal/adapters/lldap/adapter_test.go @@ -154,20 +154,16 @@ func TestLookupUser_NotFound(t *testing.T) { } } -func TestLookupUser_ValidationWarningDoesNotBlockRuntimeLogin(t *testing.T) { - // Return an entry with an empty DisplayName and empty sn. Runtime login - // should still resolve the user; provisioning validators report the warning. - dn := "uid=platform-root,ou=people,dc=netkingdom,dc=local" +func TestLookupUser_ValidationFailure(t *testing.T) { + // Return an entry with an empty DisplayName and empty sn — will fail validator. + dn := "uid=broken,ou=users,dc=netkingdom,dc=local" conn := &mockConn{ searchFn: func(req *ldap.SearchRequest) (*ldap.SearchResult, error) { - if req.BaseDN != "ou=people,dc=netkingdom,dc=local" { - t.Fatalf("BaseDN: want ou=people,dc=netkingdom,dc=local, got %q", req.BaseDN) - } attrs := []*ldap.EntryAttribute{ - {Name: "uid", Values: []string{"platform-root"}}, + {Name: "uid", Values: []string{"broken"}}, {Name: "cn", Values: []string{""}}, {Name: "sn", Values: []string{""}}, - {Name: "mail", Values: []string{"bernd.worsch@gmail.com"}}, + {Name: "mail", Values: []string{"broken@example.com"}}, } return &ldap.SearchResult{ Entries: []*ldap.Entry{{DN: dn, Attributes: attrs}}, @@ -175,21 +171,10 @@ func TestLookupUser_ValidationWarningDoesNotBlockRuntimeLogin(t *testing.T) { }, } - cfg := testConfig() - cfg.UserOU = "ou=people" - adapter := makeAdapter(cfg, conn) - user, err := adapter.LookupUser(context.Background(), "platform-root") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if user.ID != dn { - t.Errorf("ID: want %q, got %q", dn, user.ID) - } - if user.Username != "platform-root" { - t.Errorf("Username: want platform-root, got %q", user.Username) - } - if user.LDAPAttributes["_validation_warning"] == "" { - t.Error("expected validation warning for missing displayName") + adapter := makeAdapter(testConfig(), conn) + _, err := adapter.LookupUser(context.Background(), "broken") + if err == nil { + t.Fatal("expected validation error, got nil") } } diff --git a/src/internal/adapters/lldap/config.go b/src/internal/adapters/lldap/config.go index 8e91fe3..bb2b60b 100644 --- a/src/internal/adapters/lldap/config.go +++ b/src/internal/adapters/lldap/config.go @@ -6,26 +6,26 @@ package lldap // Config holds all connection parameters for the LLDAP adapter. type Config struct { // URL is the LDAP server address, e.g. "ldap://lldap:389" or "ldaps://lldap:636". - URL string `yaml:"url"` + URL string // BindDN is the distinguished name used for the service account bind, // e.g. "cn=admin,dc=netkingdom,dc=local". - BindDN string `yaml:"bindDN"` + BindDN string // BindPW is the service account password. - BindPW string `yaml:"bindPW"` + BindPW string // BaseDN is the search base, e.g. "dc=netkingdom,dc=local". - BaseDN string `yaml:"baseDN"` + BaseDN string // UserOU is the organisational unit for users. Defaults to "ou=users" when empty. - UserOU string `yaml:"userOU,omitempty"` + UserOU string // GroupOU is the organisational unit for groups. Defaults to "ou=groups" when empty. - GroupOU string `yaml:"groupOU,omitempty"` + GroupOU string // TLSSkipVerify disables TLS certificate verification. For development only. - TLSSkipVerify bool `yaml:"tlsSkipVerify,omitempty"` + TLSSkipVerify bool } // userOU returns the effective UserOU, falling back to the default. diff --git a/src/internal/adapters/privacyidea/adapter.go b/src/internal/adapters/privacyidea/adapter.go index cfdf4ce..4b07511 100644 --- a/src/internal/adapters/privacyidea/adapter.go +++ b/src/internal/adapters/privacyidea/adapter.go @@ -38,10 +38,6 @@ func New(cfg Config, httpClient HTTPClient) *PrivacyIDEAAdapter { // registered in privacyIDEA. Fails closed: any infrastructure error returns // (false, err) so callers cannot bypass the check. func (a *PrivacyIDEAAdapter) CheckMFARequired(ctx context.Context, userID string) (bool, error) { - if a.cfg.RequireForAll { - return true, nil - } - endpoint := strings.TrimRight(a.cfg.BaseURL, "/") + "/token/" q := url.Values{} diff --git a/src/internal/adapters/privacyidea/adapter_test.go b/src/internal/adapters/privacyidea/adapter_test.go index 99f07e1..670defa 100644 --- a/src/internal/adapters/privacyidea/adapter_test.go +++ b/src/internal/adapters/privacyidea/adapter_test.go @@ -101,27 +101,6 @@ func TestCheckMFARequired_ActiveTokenPresent_ReturnsTrue(t *testing.T) { } } -func TestCheckMFARequired_RequireForAll_ReturnsTrueWithoutTokenList(t *testing.T) { - client := &mockHTTPClient{ - doFn: func(_ *http.Request) (*http.Response, error) { - t.Fatal("token-list endpoint must not be called when RequireForAll is enabled") - return nil, nil - }, - } - - cfg := testConfig() - cfg.RequireForAll = true - adapter := privacyidea.New(cfg, client) - - required, err := adapter.CheckMFARequired(context.Background(), "alice") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !required { - t.Error("expected MFA required=true when RequireForAll is enabled") - } -} - func TestCheckMFARequired_InactiveTokenOnly_ReturnsFalse(t *testing.T) { client := &mockHTTPClient{ doFn: func(_ *http.Request) (*http.Response, error) { diff --git a/src/internal/adapters/privacyidea/config.go b/src/internal/adapters/privacyidea/config.go index cfe05e2..533ffc6 100644 --- a/src/internal/adapters/privacyidea/config.go +++ b/src/internal/adapters/privacyidea/config.go @@ -8,20 +8,15 @@ import "net/http" // Config holds all connection parameters for the privacyIDEA adapter. type Config struct { // BaseURL is the privacyIDEA server base URL, e.g. "https://privacyidea.local". - BaseURL string `yaml:"baseURL"` + BaseURL string // AdminToken is the service-account JWT used to authenticate requests to the // privacyIDEA admin API. - AdminToken string `yaml:"adminToken"` + AdminToken string // Realm is the privacyIDEA realm to scope token and validate requests. // Defaults to "netkingdom" when empty. - Realm string `yaml:"realm"` - - // RequireForAll skips privacyIDEA token-list discovery and requires MFA for - // every authenticated upstream user. This is useful during bootstrap when - // token-list admin credentials may not be durable yet. - RequireForAll bool `yaml:"requireForAll,omitempty"` + Realm string } // realm returns the effective realm, falling back to "netkingdom". diff --git a/src/internal/config/config_test.go b/src/internal/config/config_test.go index 73060c1..4413cd9 100644 --- a/src/internal/config/config_test.go +++ b/src/internal/config/config_test.go @@ -127,76 +127,6 @@ clients: } } -func TestLoad_PrivacyIDEARequireForAll(t *testing.T) { - keyPath := writeTempFile(t, "placeholder-key") - yaml := ` -issuer: "https://kc.example.com" -port: 8080 -tokenLifetime: "15m" -privateKeyPem: "` + keyPath + `" -environment: "dev" -privacyidea: - baseURL: "http://privacyidea.mfa.svc.cluster.local:8080" - adminToken: "service-token" - realm: "coulomb" - requireForAll: true -clients: - - clientId: "netkingdom-bootstrap-console" - displayName: "NetKingdom Bootstrap Console" - redirectUris: - - "http://127.0.0.1:8876/oidc/callback" - clientType: "public" -` - cfgPath := writeTempFile(t, yaml) - - cfg, err := config.Load(cfgPath) - if err != nil { - t.Fatalf("Load: unexpected error: %v", err) - } - if cfg.PrivacyIDEA.Realm != "coulomb" { - t.Errorf("PrivacyIDEA.Realm: got %q", cfg.PrivacyIDEA.Realm) - } - if !cfg.PrivacyIDEA.RequireForAll { - t.Error("PrivacyIDEA.RequireForAll should load from YAML") - } -} - -func TestLoad_LLDAPOrganisationalUnits(t *testing.T) { - keyPath := writeTempFile(t, "placeholder-key") - yaml := ` -issuer: "https://kc.example.com" -port: 8080 -tokenLifetime: "15m" -privateKeyPem: "` + keyPath + `" -environment: "dev" -lldap: - url: "ldap://lldap.sso.svc.cluster.local:3890" - bindDN: "uid=admin,ou=people,dc=netkingdom,dc=local" - bindPW: "secret" - baseDN: "dc=netkingdom,dc=local" - userOU: "ou=people" - groupOU: "ou=groups" -clients: - - clientId: "netkingdom-bootstrap-console" - displayName: "NetKingdom Bootstrap Console" - redirectUris: - - "http://127.0.0.1:8876/oidc/callback" - clientType: "public" -` - cfgPath := writeTempFile(t, yaml) - - cfg, err := config.Load(cfgPath) - if err != nil { - t.Fatalf("Load: unexpected error: %v", err) - } - if cfg.LLDAP.UserOU != "ou=people" { - t.Errorf("LLDAP.UserOU: got %q", cfg.LLDAP.UserOU) - } - if cfg.LLDAP.GroupOU != "ou=groups" { - t.Errorf("LLDAP.GroupOU: got %q", cfg.LLDAP.GroupOU) - } -} - func TestLoad_FileNotFound(t *testing.T) { _, err := config.Load(filepath.Join(t.TempDir(), "nonexistent.yaml")) if err == nil { diff --git a/src/internal/server/oidc/authorize.go b/src/internal/server/oidc/authorize.go index 94ef3f2..8a541d2 100644 --- a/src/internal/server/oidc/authorize.go +++ b/src/internal/server/oidc/authorize.go @@ -23,7 +23,6 @@ type PendingState struct { PKCEChallenge string PKCEChallengeMethod string State string - Nonce string Scopes []string ExpiresAt time.Time AuthenticatedUser string @@ -104,7 +103,6 @@ func (h *AuthorizeHandler) serveAuthorize(w http.ResponseWriter, r *http.Request responseType := q.Get("response_type") scope := q.Get("scope") state := q.Get("state") - nonce := q.Get("nonce") codeChallenge := q.Get("code_challenge") codeChallengeMethod := q.Get("code_challenge_method") @@ -193,7 +191,6 @@ func (h *AuthorizeHandler) serveAuthorize(w http.ResponseWriter, r *http.Request PKCEChallenge: codeChallenge, PKCEChallengeMethod: codeChallengeMethod, State: state, - Nonce: nonce, Scopes: strings.Fields(scope), ExpiresAt: time.Now().Add(10 * time.Minute), }) @@ -282,14 +279,6 @@ func (h *AuthorizeHandler) ServeHTTPCallback(w http.ResponseWriter, r *http.Requ // Check MFA requirement. mfaRequired, err := h.MFA.CheckMFARequired(ctx, result.Username) if err != nil { - h.Emitter.Emit(ctx, telemetry.Event{ - Timestamp: time.Now(), - EventType: telemetry.EventAuthFailure, - ClientID: ps.ClientID, - Endpoint: "/authorize/callback", - Result: "failure", - ErrorType: "mfa_check_error", - }) http.Error(w, "mfa check error", http.StatusInternalServerError) return } @@ -361,7 +350,6 @@ func (h *AuthorizeHandler) completeAuthorization(w http.ResponseWriter, r *http. PKCEChallenge: ps.PKCEChallenge, PKCEChallengeMethod: ps.PKCEChallengeMethod, State: ps.State, - Nonce: ps.Nonce, Username: username, Scopes: ps.Scopes, ExpiresAt: time.Now().Add(10 * time.Minute), diff --git a/src/internal/server/oidc/session.go b/src/internal/server/oidc/session.go index 72405b0..75ea6f4 100644 --- a/src/internal/server/oidc/session.go +++ b/src/internal/server/oidc/session.go @@ -15,7 +15,6 @@ type PKCESession struct { PKCEChallenge string // S256 challenge PKCEChallengeMethod string // always "S256" State string - Nonce string Username string // set after auth Scopes []string ExpiresAt time.Time diff --git a/src/internal/server/oidc/token.go b/src/internal/server/oidc/token.go index 35f1d4b..ee0848a 100644 --- a/src/internal/server/oidc/token.go +++ b/src/internal/server/oidc/token.go @@ -111,9 +111,6 @@ func (h *TokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { "exp": exp.Unix(), "iat": now.Unix(), } - if sess.Nonce != "" { - claims["nonce"] = sess.Nonce - } scopeSet := make(map[string]bool) for _, s := range sess.Scopes { diff --git a/src/internal/server/oidc/token_test.go b/src/internal/server/oidc/token_test.go index 555a432..8192c8e 100644 --- a/src/internal/server/oidc/token_test.go +++ b/src/internal/server/oidc/token_test.go @@ -107,7 +107,6 @@ func seededSession(sessions *oidc.SessionStore, verifier string) (code string) { PKCEChallenge: challenge, PKCEChallengeMethod: "S256", State: "state1", - Nonce: "nonce1", Username: "alice", Scopes: []string{"openid", "profile", "email", "groups"}, ExpiresAt: time.Now().Add(10 * time.Minute), @@ -324,9 +323,6 @@ func TestTokenHandler_JWTClaims_CorrectSubAndIssuer(t *testing.T) { if claims["aud"] != "test-client" { t.Errorf("aud: expected test-client, got %v", claims["aud"]) } - if claims["nonce"] != "nonce1" { - t.Errorf("nonce: expected nonce1, got %v", claims["nonce"]) - } } func TestTokenHandler_ScopeFiltering_ProfileScope(t *testing.T) { diff --git a/workplans/KEY-WP-0001-keycape-implementation.md b/workplans/KEY-WP-0001-keycape-implementation.md index e42fbc5..194eae4 100644 --- a/workplans/KEY-WP-0001-keycape-implementation.md +++ b/workplans/KEY-WP-0001-keycape-implementation.md @@ -2,7 +2,7 @@ id: KEY-WP-0001 type: workplan title: "KeyCape Implementation — Lightweight IAM Profile" -domain: infotech +domain: netkingdom repo: key-cape status: done owner: Bernd diff --git a/workplans/KEY-WP-0002-container-image-gitea.md b/workplans/KEY-WP-0002-container-image-gitea.md index 983d2e0..14de695 100644 --- a/workplans/KEY-WP-0002-container-image-gitea.md +++ b/workplans/KEY-WP-0002-container-image-gitea.md @@ -2,7 +2,7 @@ id: KEY-WP-0002 type: workplan title: "KeyCape Container Image — Build & Publish to Gitea OCI Registry" -domain: infotech +domain: netkingdom repo: key-cape status: done owner: netkingdom diff --git a/workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md b/workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md index 9896c67..28f2038 100644 --- a/workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md +++ b/workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md @@ -2,14 +2,13 @@ id: KEY-WP-0003 type: workplan title: "Bootstrap Console OIDC Login and MFA Verification" -domain: infotech +domain: netkingdom repo: key-cape status: finished owner: codex topic_slug: netkingdom created: "2026-05-24" updated: "2026-05-24" -state_hub_workstream_id: "02990009-a2b3-44f6-a579-487fbacae41a" --- # KEY-WP-0003 - Bootstrap Console OIDC Login and MFA Verification @@ -105,7 +104,6 @@ ceremony, and restart the KeyCape deployment. id: KEY-WP-0003-T01 status: done priority: high -state_hub_task_id: "b396c99f-d711-475a-9cba-4f03a1db561d" ``` Add a KeyCape client registration for the bootstrap console. Either create a @@ -127,7 +125,6 @@ Gate: an authorize request using the local callback no longer returns id: KEY-WP-0003-T02 status: done priority: high -state_hub_task_id: "46172e6d-3e11-493c-b223-79c2fc321aec" ``` Confirm whether the current `authelia.baseURL` is safe to use for both browser @@ -144,7 +141,6 @@ inside the deployment. id: KEY-WP-0003-T03 status: done priority: high -state_hub_task_id: "92fca4d0-6215-4ea6-9f80-9178ae183acb" ``` When `CheckMFARequired` returns true after the Authelia callback, render a @@ -167,7 +163,6 @@ browser and is returned to the registered downstream callback. id: KEY-WP-0003-T04 status: done priority: medium -state_hub_task_id: "079a5929-1864-4461-a64c-746cebca469d" ``` Add tests that cover: @@ -187,7 +182,6 @@ Gate: `make test` passes and the negative redirect URI tests remain green. id: KEY-WP-0003-T05 status: done priority: medium -state_hub_task_id: "1d67225d-a20b-4e36-9b2e-20836be2f439" ``` Document the deployment path for updating live KeyCape config without