Compare commits

..

No commits in common. "2fd69f03740c91448fb1ddc21199f91ec8f513e3" and "56d279a8e628d6a456d587d2a5a80e027157807e" have entirely different histories.

26 changed files with 54 additions and 363 deletions

View file

@ -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 "<describe your need>" --json
warden route show <catalog-id> --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`

View file

@ -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-<slug>.md ← write this first
workplans/key-cape-WP-NNNN-<slug>.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="<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}
)
```

View file

@ -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

View file

@ -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/<id>/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="<uuid>")
add_progress_event(summary="...", topic_id="a6c6e745-bf54-4465-9340-1534a2be493e", workstream_id="<uuid>")
```
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":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
-d '{"topic_id":"a6c6e745-bf54-4465-9340-1534a2be493e","workstream_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
```
If workplan files were modified, ensure the local copy is up to date first:
```bash

View file

@ -1,7 +1,7 @@
## Workplan Convention (ADR-001)
File location: `workplans/KEY-WP-NNNN-<slug>.md`
ID prefix: `KEY-WP-`
File location: `workplans/key-cape-WP-NNNN-<slug>.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-<slug>.md`. The frontmatter id remains
prefix: `YYMMDD-key-cape-WP-NNNN-<slug>.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: "<uuid>" # 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.
<!-- Ralph Loop rules and HEUREKA sequence: ~/.claude/CLAUDE.md — do not duplicate here -->

View file

@ -1,8 +1,8 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# 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.

View file

@ -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.

View file

@ -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/<task_id>" \
-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/<task_id>" \
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/<task_id>" \
---
## 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 "<describe your need>" --json
warden route show <catalog-id> --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`
<!-- REPO-AGENTS-EXTENSIONS -->
<!-- Append repo-specific agent instructions below this marker.
The state-hub template sync preserves content after this line. -->
---
## 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: "<uuid>" # written by fix-consistency — do not edit
` ` `
@ -211,7 +154,7 @@ state_hub_task_id: "<uuid>" # 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

View file

@ -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

View file

@ -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`.

View file

@ -1,4 +0,0 @@
version: 1
updated: '2026-06-16'
domain: helix_forge
capabilities: []

View file

@ -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

View file

@ -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")
}
}

View file

@ -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.

View file

@ -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{}

View file

@ -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) {

View file

@ -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".

View file

@ -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 {

View file

@ -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),

View file

@ -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

View file

@ -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 {

View file

@ -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) {

View file

@ -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

View file

@ -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

View file

@ -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