1 Commits

Author SHA1 Message Date
shankar0123 b09bd0984a auth-bundle-2 Phase 9: 11 OIDC + session MCP tools (Phase-5 surface parity)
Closes Phase 9 of cowork/auth-bundle-2-prompt.md. Every Phase-5 HTTP
endpoint now has a matching MCP tool so operators driving certctl
from Claude / VS Code / any MCP client get the same OIDC-provider +
group-mapping + session management capability the GUI + CLI already
expose.

Coverage map (each tool → HTTP endpoint → permission)
=====================================================

  certctl_auth_list_oidc_providers      GET    /v1/auth/oidc/providers                   auth.oidc.list
  certctl_auth_get_oidc_provider        GET    /v1/auth/oidc/providers (filtered)        auth.oidc.list
  certctl_auth_create_oidc_provider     POST   /v1/auth/oidc/providers                   auth.oidc.create
  certctl_auth_update_oidc_provider     PUT    /v1/auth/oidc/providers/{id}              auth.oidc.edit
  certctl_auth_delete_oidc_provider     DELETE /v1/auth/oidc/providers/{id}              auth.oidc.delete
  certctl_auth_refresh_oidc_provider    POST   /v1/auth/oidc/providers/{id}/refresh      auth.oidc.edit
  certctl_auth_list_group_mappings      GET    /v1/auth/oidc/group-mappings?provider_id  auth.oidc.list
  certctl_auth_add_group_mapping        POST   /v1/auth/oidc/group-mappings              auth.oidc.edit
  certctl_auth_remove_group_mapping     DELETE /v1/auth/oidc/group-mappings/{id}         auth.oidc.edit
  certctl_auth_list_sessions            GET    /v1/auth/sessions[?actor_id=&actor_type=] auth.session.list (own) | auth.session.list.all (other)
  certctl_auth_revoke_session           DELETE /v1/auth/sessions/{id}                    auth.session.revoke (or own-bypass)

Implementation notes
====================

internal/mcp/tools_auth_bundle2.go (NEW): 11 tools wired through three
focused register functions (registerAuthOIDCProviderTools,
registerAuthGroupMappingTools, registerAuthSessionTools). Every tool
routes through the existing Client (Get/Post/Put/Delete) so permission
gates fire server-side via the Phase-5 rbacGate wrappers — a non-admin
caller's MCP tool invocation gets whatever 403 the underlying HTTP
handler emits, not an MCP-side bypass.

Empty-id guard
--------------

Every path-id tool short-circuits to errorResult(fmt.Errorf("id is required"))
BEFORE the HTTP call. Defense against url.PathEscape("") collapsing a
singular op into the list endpoint (which would silently succeed against
a permissive backend). Same pattern across all 6 path-id tools (get,
update, delete, refresh provider; remove mapping; revoke session).

auth_get_oidc_provider list-then-filter
---------------------------------------

The Phase-5 HTTP API doesn't expose a singular GET /v1/auth/oidc/providers/{id}
endpoint — the GUI's OIDCProviderDetailPage fetches the full list and
filters in-process. The MCP tool mirrors that pattern exactly: GET the
list, JSON-decode the providers envelope, walk the array filtering by
id, return the matching raw JSON object on hit or an explicit "oidc
provider not found: <id>" error on miss. This keeps the MCP surface
in lockstep with the GUI's permission boundary (auth.oidc.list grants
"see any provider", as it does on the GUI) without inventing a new HTTP
endpoint.

internal/mcp/types.go (MODIFIED): 8 new input types matching the
Phase-5 wire shapes (oidcProviderRequest at internal/api/handler/auth_session_oidc.go).
client_secret on Update is optional — empty preserves the existing
ciphertext on the server, providing a value rotates. Mirrors the GUI's
edit-without-rotate UX from web/src/pages/auth/OIDCProviderDetailPage.tsx.

internal/mcp/tools.go (MODIFIED): registerAuthBundle2Tools wired into
RegisterTools alongside the Bundle 1 Phase 11 registerAuthTools.

Test coverage
=============

internal/mcp/tools_auth_bundle2_test.go (NEW), 5 test cases:

* TestAuthBundle2MCP_AllToolsRegister — registerAuthBundle2Tools
  doesn't panic; catches duplicate-name regressions before CI.
* TestAuthBundle2MCP_PathsAndMethods — 11 cases (one per tool) +
  the admin-other-actor variant of list_sessions; asserts the right
  method + path + body + query string fires against the mock API.
* TestAuthBundle2MCP_ForbiddenSurfacesError — every tool's underlying
  HTTP path returns a propagated error containing "forbidden" / "403"
  when the mock returns 403, exercising the errorResult fence path.
* TestAuthBundle2MCP_GetProviderFiltersListByID — pins the list-then-
  filter shape end-to-end with both the hit-and-return (returns the
  matching raw JSON object) and miss-returns-error (sentinel string
  "oidc provider not found") branches.
* TestAuthBundle2MCP_EmptyIDInputShortCircuits — pins the
  strings.TrimSpace empty-id guard at the top of every path-id handler.
* TestAuthBundle2MCP_PromptCoverage — every tool the prompt enumerates
  is also present in tools_per_tool_test.go's allHappyPathCases (so
  the live-dispatch + 5xx error-path tests cover all 11 tools).

internal/mcp/tools_per_tool_test.go (MODIFIED): 11 new toolCase entries
in allHappyPathCases (live in-memory MCP dispatch + happy-path fence
shape + 5xx error-path fence shape) + a mock-API special case for
GET /api/v1/auth/oidc/providers that returns the right envelope shape
({"providers":[{"id":"op-okta",...}]}) so the get_oidc_provider tool's
in-process filter resolves under the live dispatch.

Verification
============

* gofmt + go vet — clean across internal/mcp/...
* go test -short -count=1 — green across internal/mcp + internal/auth/...
  + internal/api/handler + internal/api/router (13 packages, 0 failures).
* MCP tool count re-derive (CLAUDE.md command):
    grep -cE 'mcp\.AddTool\(' internal/mcp/tools*.go
  → tools.go=121, tools_auth.go=12, tools_auth_bundle2.go=11 (new),
  tools_est.go=6 — total 150. Matches the live count
  TestMCP_RegisterTools_DispatchableToolCount asserts.
* staticcheck deferred — sandbox /tmp at 99% disk, can't install the
  binary; all SA*/ST* lints would have run via the staticcheck-CI step
  on push. go vet caught the only real issue (an unused context import)
  before commit.

Not in this commit (deferred)
=============================

* Break-glass admin MCP tools (4 endpoints from Phase 7.5). The Phase 9
  prompt does NOT enumerate break-glass tools; its exit criteria is
  "Every API endpoint from Phase 5 has an MCP tool". Phase 5 does not
  include the break-glass surface (Phase 7.5 ships those endpoints with
  surface-invisibility semantics: 404 when CERTCTL_BREAKGLASS_ENABLED=false,
  which complicates LLM tool-discovery UX). If the operator wants
  break-glass MCP parity, that's a follow-on bundle.
2026-05-10 07:40:34 +00:00