Compare commits
No commits in common. "2fd69f03740c91448fb1ddc21199f91ec8f513e3" and "56d279a8e628d6a456d587d2a5a80e027157807e" have entirely different histories.
2fd69f0374
...
56d279a8e6
26 changed files with 54 additions and 363 deletions
|
|
@ -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`
|
||||
|
|
@ -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}
|
||||
)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
75
AGENTS.md
75
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/<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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
version: 1
|
||||
updated: '2026-06-16'
|
||||
domain: helix_forge
|
||||
capabilities: []
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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".
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue