From 83f1155e8910449486de62e51c01b1a1a4289e04 Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Fri, 24 Apr 2026 23:51:36 -0400 Subject: [PATCH] fix: match tenant-prefixed MCP scopes --- pkg/mcpserver/proxy.go | 12 ++++++++---- pkg/mcpserver/scope_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 pkg/mcpserver/scope_test.go diff --git a/pkg/mcpserver/proxy.go b/pkg/mcpserver/proxy.go index 1201e950..06f39a95 100644 --- a/pkg/mcpserver/proxy.go +++ b/pkg/mcpserver/proxy.go @@ -796,10 +796,14 @@ func matchScope(scope, toolName string) bool { for _, s := range strings.Split(scope, ",") { s = strings.TrimSpace(s) - // Strip tenant UUID prefix if present (format: "uuid:scope") - // UUIDs are 36 chars with hyphens (8-4-4-4-12) - if len(s) > 37 && s[36] == ':' && s[8] == '-' && s[13] == '-' { - s = s[37:] + // Strip tenant prefix if present (format: "tenant-id:scope"). + // SaaS tokens are minted with tenant-prefixed scopes such as + // "demo-82712860:*"; older logic only handled UUID tenant IDs. + if idx := strings.IndexByte(s, ':'); idx > 0 { + rest := s[idx+1:] + if rest == "*" || rest == "api:*" || rest == "mcp:*" || strings.HasSuffix(rest, ":*") { + s = rest + } } if s == "*" || s == "api:*" || s == "mcp:*" { diff --git a/pkg/mcpserver/scope_test.go b/pkg/mcpserver/scope_test.go new file mode 100644 index 00000000..f7aad977 --- /dev/null +++ b/pkg/mcpserver/scope_test.go @@ -0,0 +1,28 @@ +package mcpserver + +import "testing" + +func TestMatchScopeTenantPrefixedWildcard(t *testing.T) { + tests := []struct { + name string + scope string + toolName string + want bool + }{ + {name: "tenant slug wildcard", scope: "demo-82712860:*", toolName: "ping", want: true}, + {name: "tenant slug mcp wildcard", scope: "demo-82712860:mcp:*", toolName: "ping", want: true}, + {name: "tenant slug namespaced wildcard", scope: "demo-82712860:db:*", toolName: "db:query", want: true}, + {name: "uuid wildcard", scope: "123e4567-e89b-12d3-a456-426614174000:*", toolName: "ping", want: true}, + {name: "unrelated exact colon tool remains exact", scope: "db:query", toolName: "db:query", want: true}, + {name: "unrelated exact colon tool does not widen", scope: "db:query", toolName: "query", want: false}, + {name: "tenant prefix does not widen exact unrelated", scope: "demo-82712860:db:query", toolName: "query", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := matchScope(tt.scope, tt.toolName); got != tt.want { + t.Fatalf("matchScope(%q, %q) = %v, want %v", tt.scope, tt.toolName, got, tt.want) + } + }) + } +}