Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
66660d6
feat(authz): add accesstoken definition and platform token permissions
josephschorr Oct 1, 2026
e3f62be
test(authz): pin accesstoken relation lines exactly; gofmt test files
josephschorr Oct 1, 2026
2d397e2
feat(identity): shared accesstoken names and token-value helpers
josephschorr Oct 1, 2026
c96df1f
feat(authz): SpiceDB write/check/read helpers for access tokens
josephschorr Oct 1, 2026
a2d955e
test(authz): unit-cover access-token helpers; guard internal permissi…
josephschorr Oct 1, 2026
cab8b0c
feat(apis): AccessToken CRD (authentication record for OAuth-minted t…
josephschorr Oct 1, 2026
9f54f8c
feat(controllers): AccessToken reconciler — finalizer tuple cleanup a…
josephschorr Oct 1, 2026
f773274
fix(controllers): read the clock once per AccessToken reconcile so a …
josephschorr Oct 1, 2026
6bc6da7
feat(identityd): OAuth AS metadata and stateless dynamic client regis…
josephschorr Oct 1, 2026
b506df8
docs(identityd): note why the DCR decoder deliberately permits unknow…
josephschorr Oct 1, 2026
b0d600f
feat(identityd): OAuth authorize endpoint, consent screen, auth-code …
josephschorr Oct 1, 2026
ec782f0
feat(mcpfront,identityd): token endpoint and tuple-first access-token…
josephschorr Oct 1, 2026
9859cd5
fix(installcmd): cover accesstokens in oap clean's CRD teardown list
josephschorr Oct 1, 2026
adfbffa
feat(mcpfront): /mcp plugin with bearer middleware and protected-reso…
josephschorr Oct 1, 2026
8b57b7d
fix(mcpfront): rate-limiter state survives cache rebuilds; miss-trigg…
josephschorr Oct 1, 2026
3d08598
feat(mcpfront): read-role typed ops and MCP tools
josephschorr Oct 1, 2026
1df00b2
fix(mcpfront,authz): leg-3 checks use real resource permissions for v…
josephschorr Oct 1, 2026
60e3cee
feat(admin): access-token list and revoke page
josephschorr Oct 2, 2026
6ca9c62
feat(oap): setup-mcp command emitting local-tool MCP config
josephschorr Oct 2, 2026
0c49e98
test(mcpfront): end-to-end OAuth + MCP flow, revocation, scope and ow…
josephschorr Oct 2, 2026
708623e
fix(rbac): minimize accesstoken grants and add their sufficiency rows
josephschorr Oct 2, 2026
b053151
docs,fix: final-review wave — reference regen, multi-replica caveat, …
josephschorr Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/oap/internal/installcmd/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ var crGroupVersionResources = []schema.GroupVersionResource{
// against Kubernetes' own owner-reference garbage collection.
{Group: "agentprimitives.authzed.com", Version: "v1alpha1", Resource: "subagentrequests"},
{Group: "agentprimitives.authzed.com", Version: "v1alpha1", Resource: "relationshipsources"},
// AccessToken carries FinalizerAccessToken, cleared only by the
// operator's accesstoken controller (pkg/controllers/accesstoken)
// removing the token's SpiceDB tuples. The PublicEndpoint concern above
// applies: omitting it here would wedge teardown on a finalizer nothing
// is left running to clear.
{Group: "agentprimitives.authzed.com", Version: "v1alpha1", Resource: "accesstokens"},
}

// cleanDefaultTimeout is `oap clean`'s own budget for a full teardown. Named
Expand Down
117 changes: 117 additions & 0 deletions cmd/oap/internal/setupmcpcmd/emit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package setupmcpcmd

import (
"encoding/json"
"fmt"
"strings"
)

// mcpURL appends "/mcp" to serverURL, trimming any trailing slash first so
// the result never contains a doubled slash. Every emitter routes the URL it
// prints through this function, so "the emitted URL always ends in /mcp" is
// enforced in one place rather than re-derived per tool.
func mcpURL(serverURL string) string {
return strings.TrimRight(serverURL, "/") + "/mcp"
}

// authHeaders returns the single Authorization header map for bearer, or nil
// when bearer is empty. nil (not an empty map) so callers can tag the field
// `omitempty` and have it vanish entirely from unauthenticated output, rather
// than rendering as `"headers": {}`.
func authHeaders(bearer string) map[string]string {
if bearer == "" {
return nil
}
return map[string]string{"Authorization": "Bearer " + bearer}
}

// mcpServerEntry is the common shape both Claude Code's and Cursor's
// mcpServers map use.
type mcpServerEntry struct {
Type string `json:"type,omitempty"`
URL string `json:"url"`
Headers map[string]string `json:"headers,omitempty"`
}

// emitClaudeCode renders the `claude mcp add` command for this server plus
// the equivalent mcpServers JSON snippet, for a user who'd rather hand-edit
// their config. bearer, when non-empty, is embedded verbatim (never
// truncated) as an Authorization header in both the --header flag and the
// JSON snippet.
func emitClaudeCode(serverURL, bearer string) string {
url := mcpURL(serverURL)

cmd := fmt.Sprintf("claude mcp add --transport http oap %s", url)
if bearer != "" {
cmd += fmt.Sprintf(" --header %s", quoteShellArg(fmt.Sprintf("Authorization: Bearer %s", bearer)))
}

snippet := struct {
MCPServers map[string]mcpServerEntry `json:"mcpServers"`
}{
MCPServers: map[string]mcpServerEntry{
"oap": {
Type: "http",
URL: url,
Headers: authHeaders(bearer),
},
},
}
b, err := json.MarshalIndent(snippet, "", " ")
if err != nil {
// snippet is a fixed shape of strings and maps of strings; it cannot fail
// to marshal. Panic rather than silently drop the JSON half of the output.
panic(fmt.Sprintf("setupmcpcmd: marshal claude-code snippet: %v", err))
}

return cmd + "\n\n" + string(b) + "\n"
}

// emitCursor renders the .cursor/mcp.json snippet for this server. The whole
// output is valid JSON.
func emitCursor(serverURL, bearer string) string {
url := mcpURL(serverURL)

snippet := struct {
MCPServers map[string]mcpServerEntry `json:"mcpServers"`
}{
MCPServers: map[string]mcpServerEntry{
"oap": {
URL: url,
Headers: authHeaders(bearer),
},
},
}
b, err := json.MarshalIndent(snippet, "", " ")
if err != nil {
panic(fmt.Sprintf("setupmcpcmd: marshal cursor snippet: %v", err))
}
return string(b) + "\n"
}

// emitGeneric renders a minimal {"url": ..., "headers": {...}} snippet for any
// MCP client that doesn't have a dedicated emitter. The whole output is valid
// JSON.
func emitGeneric(serverURL, bearer string) string {
snippet := struct {
URL string `json:"url"`
Headers map[string]string `json:"headers,omitempty"`
}{
URL: mcpURL(serverURL),
Headers: authHeaders(bearer),
}
b, err := json.MarshalIndent(snippet, "", " ")
if err != nil {
panic(fmt.Sprintf("setupmcpcmd: marshal generic snippet: %v", err))
}
return string(b) + "\n"
}

// quoteShellArg wraps s in double quotes for display in a copy-pasteable
// shell command line. s here is always "Authorization: Bearer <token>",
// which contains no double quotes or backslashes, so a full shell-quoting
// library is not needed — this only ever has to survive display, not
// execution.
func quoteShellArg(s string) string {
return `"` + s + `"`
}
162 changes: 162 additions & 0 deletions cmd/oap/internal/setupmcpcmd/emit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package setupmcpcmd

import (
"encoding/json"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// longBearer is deliberately long and distinctive so a test can catch any
// truncation/elision (e.g. a "..." middle-out summary) that a naive display
// helper might apply.
const longBearer = "oap_mcp_access_token_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.signature-part-that-is-also-quite-long-1234567890"

func TestEmitClaudeCode(t *testing.T) {
cases := []struct {
name string
bearer string
}{
{name: "with bearer", bearer: longBearer},
{name: "without bearer", bearer: ""},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
out := emitClaudeCode("https://cluster.example.com", tc.bearer)

assert.Contains(t, out, "https://cluster.example.com/mcp", "emitted URL must end in /mcp")
assert.Contains(t, out, "claude mcp add --transport http oap https://cluster.example.com/mcp")

if tc.bearer != "" {
assert.Contains(t, out, tc.bearer, "bearer must appear verbatim, never truncated")
assert.Contains(t, out, `--header "Authorization: Bearer `+tc.bearer+`"`)
} else {
assert.NotContains(t, out, "--header")
assert.NotContains(t, out, "Authorization")
}

// The command line is followed by a blank line and then a JSON
// snippet; extract and validate that part unmarshals cleanly.
idx := strings.Index(out, "{")
require.GreaterOrEqual(t, idx, 0, "expected a JSON snippet in the output")
jsonPart := out[idx:]

var decoded struct {
MCPServers map[string]struct {
Type string `json:"type"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
} `json:"mcpServers"`
}
require.NoError(t, json.Unmarshal([]byte(jsonPart), &decoded), "JSON snippet must unmarshal cleanly")

entry, ok := decoded.MCPServers["oap"]
require.True(t, ok, "expected an \"oap\" entry in mcpServers")
assert.Equal(t, "https://cluster.example.com/mcp", entry.URL)
assert.Equal(t, "http", entry.Type)

if tc.bearer != "" {
require.NotNil(t, entry.Headers)
assert.Equal(t, "Bearer "+tc.bearer, entry.Headers["Authorization"])
} else {
assert.Empty(t, entry.Headers)
}
})
}
}

func TestEmitCursor(t *testing.T) {
cases := []struct {
name string
bearer string
}{
{name: "with bearer", bearer: longBearer},
{name: "without bearer", bearer: ""},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
out := emitCursor("https://cluster.example.com/", tc.bearer)

assert.Contains(t, out, "https://cluster.example.com/mcp", "emitted URL must end in /mcp, even when serverURL had a trailing slash")
assert.NotContains(t, out, "//mcp", "trailing slash on serverURL must not produce a doubled slash")

var decoded struct {
MCPServers map[string]struct {
URL string `json:"url"`
Headers map[string]string `json:"headers"`
} `json:"mcpServers"`
}
require.NoError(t, json.Unmarshal([]byte(out), &decoded), "entire output must be valid JSON")

entry, ok := decoded.MCPServers["oap"]
require.True(t, ok, "expected an \"oap\" entry in mcpServers")
assert.Equal(t, "https://cluster.example.com/mcp", entry.URL)

if tc.bearer != "" {
assert.Contains(t, out, tc.bearer, "bearer must appear verbatim, never truncated")
require.NotNil(t, entry.Headers)
assert.Equal(t, "Bearer "+tc.bearer, entry.Headers["Authorization"])
} else {
assert.NotContains(t, out, "Authorization")
assert.Empty(t, entry.Headers)
}
})
}
}

func TestEmitGeneric(t *testing.T) {
cases := []struct {
name string
bearer string
}{
{name: "with bearer", bearer: longBearer},
{name: "without bearer", bearer: ""},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
out := emitGeneric("https://cluster.example.com", tc.bearer)

assert.Contains(t, out, "https://cluster.example.com/mcp")

var decoded struct {
URL string `json:"url"`
Headers map[string]string `json:"headers"`
}
require.NoError(t, json.Unmarshal([]byte(out), &decoded), "entire output must be valid JSON")
assert.Equal(t, "https://cluster.example.com/mcp", decoded.URL)

if tc.bearer != "" {
assert.Contains(t, out, tc.bearer, "bearer must appear verbatim, never truncated")
require.NotNil(t, decoded.Headers)
assert.Equal(t, "Bearer "+tc.bearer, decoded.Headers["Authorization"])
} else {
assert.NotContains(t, out, "Authorization")
assert.Empty(t, decoded.Headers)
}
})
}
}

// TestEmit_AllToolsAppendMCPSuffix is a compact cross-tool sanity pass: every
// emitter, regardless of whether serverURL already ends in a slash, must
// route the URL through the same "/mcp" suffix with no doubled slash.
func TestEmit_AllToolsAppendMCPSuffix(t *testing.T) {
emitters := map[string]func(serverURL, bearer string) string{
"claude-code": emitClaudeCode,
"cursor": emitCursor,
"generic": emitGeneric,
}

for name, emit := range emitters {
t.Run(name, func(t *testing.T) {
out := emit("https://cluster.example.com", "")
assert.True(t, strings.Contains(out, "https://cluster.example.com/mcp"), "expected /mcp suffix in output")
assert.False(t, strings.Contains(out, "mcp/mcp"), "must not double-append /mcp")
})
}
}
Loading
Loading