diff --git a/pkg/authz/authorizers/cedar/annotations_integration_test.go b/pkg/authz/authorizers/cedar/annotations_integration_test.go index c36a90f552..e7cb9fb81a 100644 --- a/pkg/authz/authorizers/cedar/annotations_integration_test.go +++ b/pkg/authz/authorizers/cedar/annotations_integration_test.go @@ -98,7 +98,7 @@ func TestAuthorizeWithToolAnnotations(t *testing.T) { authzr, err := NewCedarAuthorizer(ConfigOptions{ Policies: tc.policies, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err, "Failed to create Cedar authorizer") // Build context with identity diff --git a/pkg/authz/authorizers/cedar/annotations_override_test.go b/pkg/authz/authorizers/cedar/annotations_override_test.go index b6161c1737..6bc7c6fb92 100644 --- a/pkg/authz/authorizers/cedar/annotations_override_test.go +++ b/pkg/authz/authorizers/cedar/annotations_override_test.go @@ -128,7 +128,7 @@ func TestAnnotationAttributesCannotOverrideStandardAttributes(t *testing.T) { authzr, err := NewCedarAuthorizer(ConfigOptions{ Policies: tc.policies, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err, "Failed to create Cedar authorizer") ctx := t.Context() diff --git a/pkg/authz/authorizers/cedar/core.go b/pkg/authz/authorizers/cedar/core.go index 4c173085b4..122b3e6e20 100644 --- a/pkg/authz/authorizers/cedar/core.go +++ b/pkg/authz/authorizers/cedar/core.go @@ -119,7 +119,7 @@ func (*Factory) ValidateConfig(rawConfig json.RawMessage) error { // CreateAuthorizer creates a Cedar Authorizer from the configuration. // It receives the full raw config and extracts the Cedar-specific portion. -func (*Factory) CreateAuthorizer(rawConfig json.RawMessage, _ string) (authorizers.Authorizer, error) { +func (*Factory) CreateAuthorizer(rawConfig json.RawMessage, serverName string) (authorizers.Authorizer, error) { var config Config if err := json.Unmarshal(rawConfig, &config); err != nil { return nil, fmt.Errorf("failed to parse configuration: %w", err) @@ -129,7 +129,7 @@ func (*Factory) CreateAuthorizer(rawConfig json.RawMessage, _ string) (authorize return nil, fmt.Errorf("cedar configuration is required (missing 'cedar' field)") } - return NewCedarAuthorizer(*config.Options) + return NewCedarAuthorizer(*config.Options, serverName) } // Common errors for Cedar authorization @@ -167,6 +167,12 @@ type Authorizer struct { // roleClaimName is the JWT claim key that contains role membership. // When empty, no role extraction is performed (backward compatible). roleClaimName string + // serverName is the identity of the MCP server this authorizer is scoped to. + // Used by downstream enterprise features for server-scoped Cedar policies + // (e.g. resource in MCP::""). When empty (standalone Cedar usage + // with no enterprise controller), the authorizer behaves identically to + // the unscoped case. + serverName string // claimKeyLog rate-limits the diagnostic log of resolved JWT claim keys // so it emits at most once per 30 seconds instead of once per authorization check. claimKeyLog *syncutil.AtMost @@ -201,7 +207,11 @@ type ConfigOptions struct { } // NewCedarAuthorizer creates a new Cedar authorizer. -func NewCedarAuthorizer(options ConfigOptions) (authorizers.Authorizer, error) { +// serverName is a runtime-injected value (not user-authored config) that +// identifies which MCP server this authorizer is scoped to. +// If a second runtime-injected value is needed, bundle both into a +// RuntimeContext struct to keep the factory interface stable. +func NewCedarAuthorizer(options ConfigOptions, serverName string) (authorizers.Authorizer, error) { authorizer := &Authorizer{ policySet: cedar.NewPolicySet(), entities: cedar.EntityMap{}, @@ -209,6 +219,7 @@ func NewCedarAuthorizer(options ConfigOptions) (authorizers.Authorizer, error) { primaryUpstreamProvider: options.PrimaryUpstreamProvider, groupClaimName: options.GroupClaimName, roleClaimName: options.RoleClaimName, + serverName: serverName, claimKeyLog: syncutil.NewAtMost(30 * time.Second), } diff --git a/pkg/authz/authorizers/cedar/core_test.go b/pkg/authz/authorizers/cedar/core_test.go index 92976cbff6..416f51da8d 100644 --- a/pkg/authz/authorizers/cedar/core_test.go +++ b/pkg/authz/authorizers/cedar/core_test.go @@ -37,9 +37,11 @@ func TestNewCedarAuthorizer(t *testing.T) { policies []string entitiesJSON string roleClaimName string + serverName string expectError bool errorType error wantRoleClaimName string + wantServerName string }{ { name: "Valid policy and empty entities", @@ -94,6 +96,14 @@ func TestNewCedarAuthorizer(t *testing.T) { expectError: false, wantRoleClaimName: "https://example.com/roles", }, + { + name: "Stores server name", + policies: []string{`permit(principal, action, resource);`}, + entitiesJSON: `[]`, + serverName: "my-mcp-server", + expectError: false, + wantServerName: "my-mcp-server", + }, } // Run test cases @@ -105,7 +115,7 @@ func TestNewCedarAuthorizer(t *testing.T) { Policies: tc.policies, EntitiesJSON: tc.entitiesJSON, RoleClaimName: tc.roleClaimName, - }) + }, tc.serverName) // Check error expectations if tc.expectError { @@ -121,6 +131,7 @@ func TestNewCedarAuthorizer(t *testing.T) { cedarAuthz, ok := authorizer.(*Authorizer) require.True(t, ok) assert.Equal(t, tc.wantRoleClaimName, cedarAuthz.roleClaimName) + assert.Equal(t, tc.wantServerName, cedarAuthz.serverName) } }) } @@ -351,7 +362,7 @@ func TestAuthorizeWithJWTClaims(t *testing.T) { authorizer, err := NewCedarAuthorizer(ConfigOptions{ Policies: []string{tc.policy}, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err, "Failed to create Cedar authorizer") // Create a context with JWT claims @@ -376,7 +387,7 @@ func TestAuthorizeWithJWTClaimsErrors(t *testing.T) { authorizer, err := NewCedarAuthorizer(ConfigOptions{ Policies: []string{`permit(principal, action, resource);`}, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err, "Failed to create Cedar authorizer") // Test cases @@ -675,6 +686,10 @@ func TestFactoryCreateAuthorizer(t *testing.T) { require.NoError(t, err) require.NotNil(t, authorizer) + + cedarAuthz, ok := authorizer.(*Authorizer) + require.True(t, ok) + assert.Equal(t, "testServer", cedarAuthz.serverName) }) } } @@ -687,7 +702,7 @@ func TestUpdatePolicies(t *testing.T) { authorizer, err := NewCedarAuthorizer(ConfigOptions{ Policies: []string{`permit(principal, action, resource);`}, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err) // Cast to concrete type to access UpdatePolicies @@ -748,7 +763,7 @@ func TestUpdateEntities(t *testing.T) { authorizer, err := NewCedarAuthorizer(ConfigOptions{ Policies: []string{`permit(principal, action, resource);`}, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err) // Cast to concrete type to access UpdateEntities @@ -799,7 +814,7 @@ func TestEntityOperations(t *testing.T) { authorizer, err := NewCedarAuthorizer(ConfigOptions{ Policies: []string{`permit(principal, action, resource);`}, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err) // Cast to concrete type to access entity methods @@ -839,7 +854,7 @@ func TestGetEntityNotFound(t *testing.T) { authorizer, err := NewCedarAuthorizer(ConfigOptions{ Policies: []string{`permit(principal, action, resource);`}, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err) // Cast to concrete type @@ -863,7 +878,7 @@ func TestIsAuthorizedErrors(t *testing.T) { authorizer, err := NewCedarAuthorizer(ConfigOptions{ Policies: []string{`permit(principal, action, resource);`}, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err) // Cast to concrete type @@ -958,7 +973,7 @@ func TestIsAuthorizedWithEntities(t *testing.T) { ); `}, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err) // Cast to concrete type @@ -1097,7 +1112,7 @@ func TestAuthorizeWithJWTClaims_UpstreamProvider(t *testing.T) { Policies: []string{policy}, EntitiesJSON: `[]`, PrimaryUpstreamProvider: providerName, - }) + }, "") require.NoError(t, err) upstreamToken := makeUnsignedJWT(jwt.MapClaims{ @@ -1242,7 +1257,7 @@ func TestAuthorizeWithJWTClaims_GroupMembership(t *testing.T) { authorizer, err := NewCedarAuthorizer(ConfigOptions{ Policies: []string{policy}, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err) tests := []struct { @@ -1313,7 +1328,7 @@ func TestAuthorizeWithJWTClaims_DoesNotMutateIdentity(t *testing.T) { authorizer, err := NewCedarAuthorizer(ConfigOptions{ Policies: []string{policy}, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err) identity := &auth.Identity{ @@ -1361,7 +1376,7 @@ func TestAuthorizeWithJWTClaims_CustomGroupClaimName(t *testing.T) { Policies: []string{policy}, EntitiesJSON: `[]`, GroupClaimName: "https://example.com/groups", - }) + }, "") require.NoError(t, err) // The custom claim holds "platform"; the well-known "groups" key holds other groups. @@ -1410,7 +1425,7 @@ func TestAuthorizeWithJWTClaims_UpstreamProviderWithGroups(t *testing.T) { Policies: []string{policy}, EntitiesJSON: `[]`, PrimaryUpstreamProvider: providerName, - }) + }, "") require.NoError(t, err) tests := []struct { diff --git a/pkg/authz/integration_test.go b/pkg/authz/integration_test.go index 990a298b45..4c17bd6dc3 100644 --- a/pkg/authz/integration_test.go +++ b/pkg/authz/integration_test.go @@ -48,7 +48,7 @@ func TestIntegrationListFiltering(t *testing.T) { `permit(principal, action == Action::"read_resource", resource) when { principal.claim_role == "admin" };`, }, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err, "Failed to create Cedar authorizer") testCases := []struct { @@ -335,7 +335,7 @@ func TestIntegrationNonListOperations(t *testing.T) { `permit(principal, action == Action::"call_tool", resource) when { principal.claim_role == "admin" };`, }, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err, "Failed to create Cedar authorizer") testCases := []struct { diff --git a/pkg/authz/middleware_test.go b/pkg/authz/middleware_test.go index 2def6ff1a0..2c3a815561 100644 --- a/pkg/authz/middleware_test.go +++ b/pkg/authz/middleware_test.go @@ -61,7 +61,7 @@ func TestMiddleware(t *testing.T) { `permit(principal, action == Action::"read_resource", resource == Resource::"data");`, }, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err, "Failed to create Cedar authorizer") // Test cases @@ -443,7 +443,7 @@ func TestMiddlewareWithGETRequest(t *testing.T) { `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`, }, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err, "Failed to create Cedar authorizer") // Create a handler that records if it was called @@ -808,7 +808,7 @@ func TestMiddlewareToolsListTestkit(t *testing.T) { cedar.ConfigOptions{ Policies: tc.policies, EntitiesJSON: `[]`, - }, + }, "", ) require.NoError(t, err, "Failed to create Cedar authorizer") @@ -978,7 +978,7 @@ func TestMiddlewareToolsCallTestkit(t *testing.T) { cedar.ConfigOptions{ Policies: tc.policies, EntitiesJSON: `[]`, - }, + }, "", ) require.NoError(t, err, "Failed to create Cedar authorizer") @@ -1049,7 +1049,7 @@ func TestMiddlewareOptimizerMetaTools(t *testing.T) { `permit(principal, action == Action::"call_tool", resource == Tool::"allowed_backend");`, }, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err) passThroughTools := map[string]struct{}{ @@ -1150,7 +1150,7 @@ func TestMiddlewareOptimizerCallToolJSONRoundTrip(t *testing.T) { `permit(principal, action == Action::"call_tool", resource == Tool::"backend_fetch");`, }, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err) passThroughTools := map[string]struct{}{ diff --git a/pkg/authz/response_filter_test.go b/pkg/authz/response_filter_test.go index 450068e184..f36c70e772 100644 --- a/pkg/authz/response_filter_test.go +++ b/pkg/authz/response_filter_test.go @@ -88,7 +88,7 @@ func TestFindToolResponseFilter(t *testing.T) { `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`, }, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err) identity := &auth.Identity{PrincipalInfo: auth.PrincipalInfo{ @@ -239,7 +239,7 @@ func TestResponseFilteringWriter(t *testing.T) { `permit(principal, action == Action::"read_resource", resource == Resource::"data");`, }, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err, "Failed to create Cedar authorizer") testCases := []struct { @@ -432,7 +432,7 @@ func TestResponseFilteringWriter_NonListOperations(t *testing.T) { `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`, }, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err, "Failed to create Cedar authorizer") // Test that non-list operations pass through unchanged @@ -481,7 +481,7 @@ func TestResponseFilteringWriter_ErrorResponse(t *testing.T) { `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`, }, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err, "Failed to create Cedar authorizer") // Create an error response @@ -534,7 +534,7 @@ func TestResponseFilteringWriter_ContentLengthMismatch(t *testing.T) { `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`, }, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err, "Failed to create Cedar authorizer") // Build the backend response: a tools/list result with 3 tools. @@ -727,7 +727,7 @@ func TestOptimizerPassThroughToolsInResponseFilter(t *testing.T) { `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`, }, EntitiesJSON: "[]", - }) + }, "") require.NoError(t, err) // Build a tools/list response as the optimizer would produce it: diff --git a/pkg/authz/tool_filter_test.go b/pkg/authz/tool_filter_test.go index 061eda93ef..0043de1526 100644 --- a/pkg/authz/tool_filter_test.go +++ b/pkg/authz/tool_filter_test.go @@ -181,7 +181,7 @@ func TestFilterToolsByPolicy_WithCedarAuthorizer(t *testing.T) { `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`, }, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err) t.Run("keeps only permitted tool", func(t *testing.T) { @@ -222,7 +222,7 @@ func TestAuthorizeToolCall_WithCedarAuthorizer(t *testing.T) { `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`, }, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err) t.Run("permits authorized tool", func(t *testing.T) { @@ -252,7 +252,7 @@ func TestAuthorizeToolCall_WithArguments(t *testing.T) { `permit(principal, action == Action::"call_tool", resource == Tool::"deploy") when { context.arg_mode == "safe" };`, }, EntitiesJSON: `[]`, - }) + }, "") require.NoError(t, err) t.Run("permits when arguments satisfy policy", func(t *testing.T) {