diff --git a/gateway/gateway-runtime/policy-engine/internal/kernel/analytics.go b/gateway/gateway-runtime/policy-engine/internal/kernel/analytics.go index 9a891e002..30d5ba19b 100644 --- a/gateway/gateway-runtime/policy-engine/internal/kernel/analytics.go +++ b/gateway/gateway-runtime/policy-engine/internal/kernel/analytics.go @@ -36,6 +36,7 @@ const ( OperationPathKey = Wso2MetadataPrefix + "operation-path" APIKindKey = Wso2MetadataPrefix + "api-kind" ProjectIDKey = Wso2MetadataPrefix + "project-id" + UserIDKey = Wso2MetadataPrefix + "user-id" ) // convertToStructValue converts a value to structpb.Value, handling complex types like map[string][]string @@ -96,6 +97,10 @@ func buildAnalyticsStruct(analyticsData map[string]any, execCtx *PolicyExecution if sharedCtx.ProjectID != "" { fields[ProjectIDKey] = structpb.NewStringValue(sharedCtx.ProjectID) } + // Extract UserID from AuthContext if available + if sharedCtx.AuthContext != nil && sharedCtx.AuthContext.UserID != "" { + fields[UserIDKey] = structpb.NewStringValue(sharedCtx.AuthContext.UserID) + } } return &structpb.Struct{Fields: fields}, nil diff --git a/gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go b/gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go index dfdd00028..bfad31932 100644 --- a/gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go +++ b/gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go @@ -359,7 +359,10 @@ func (ec *PolicyExecutionContext) buildRequestContext(headers *extprocv3.HttpHea APIContext: routeMetadata.Context, OperationPath: routeMetadata.OperationPath, Metadata: make(map[string]interface{}), - AuthContext: make(map[string]string), + AuthContext: &policy.AuthContext{ + Properties: make(map[string]string), + Scopes: make(map[string]bool), + }, } // Add template handle to metadata for LLM provider/proxy scenarios if routeMetadata.TemplateHandle != "" { diff --git a/gateway/system-policies/analytics/analytics.go b/gateway/system-policies/analytics/analytics.go index 693e09291..1ac3c8ec3 100644 --- a/gateway/system-policies/analytics/analytics.go +++ b/gateway/system-policies/analytics/analytics.go @@ -31,9 +31,6 @@ const ( AIProviderNameMetadataKey = "ai:providername" AIProviderDisplayNameMetadataKey = "ai:providerdisplayname" - // AuthContext key for user ID (used for analytics) - AuthContextKeyUserID = "x-wso2-user-id" - // Lazy resource type for LLM provider templates lazyResourceTypeLLMProviderTemplate = "LlmProviderTemplate" // Lazy resource type for provider-to-template mapping @@ -59,22 +56,22 @@ var ( type AnalyticsPolicy struct{} type McpRequestAnalyticsProperties struct { - JsonRpcMethod string `json:"jsonRpcMethod,omitempty"` - Capability string `json:"capability,omitempty"` - CapabilityName string `json:"capabilityName,omitempty"` - ClientInfo *McpClientInfo `json:"clientInfo,omitempty"` + JsonRpcMethod string `json:"jsonRpcMethod,omitempty"` + Capability string `json:"capability,omitempty"` + CapabilityName string `json:"capabilityName,omitempty"` + ClientInfo *McpClientInfo `json:"clientInfo,omitempty"` ServerInfo *McpServerInfo `json:"serverInfo,omitempty"` } type McpClientInfo struct { - RequestedProtocolVersion string `json:"requestedProtocolVersion"` - Name string `json:"name"` - Version string `json:"version"` + RequestedProtocolVersion string `json:"requestedProtocolVersion"` + Name string `json:"name"` + Version string `json:"version"` } type McpServerInfo struct { - ProtocolVersion string `json:"protocolVersion,omitempty"` - ServerInfo *McpServerInfoDetails `json:"serverInfo,omitempty"` + ProtocolVersion string `json:"protocolVersion,omitempty"` + ServerInfo *McpServerInfoDetails `json:"serverInfo,omitempty"` } type McpServerInfoDetails struct { @@ -83,8 +80,8 @@ type McpServerInfoDetails struct { } type McpResponseAnalyticsProperties struct { - IsError bool `json:"isError,omitempty"` - ErrorCode int `json:"errorCode,omitempty"` + IsError bool `json:"isError,omitempty"` + ErrorCode int `json:"errorCode,omitempty"` } // LLMTokenInfo holds extracted token-related information from LLM provider responses @@ -123,7 +120,7 @@ func (a *AnalyticsPolicy) Mode() policy.ProcessingMode { func (a *AnalyticsPolicy) OnRequest(ctx *policy.RequestContext, params map[string]interface{}) policy.RequestAction { slog.Debug("Analytics system policy: OnRequest called") allowPayloads := getAllowPayloadsFlag(params) - // Store tokenInfo in analytics metadata for publishing + // Store tokenInfo in analytics metadata for publishing analyticsMetadata := make(map[string]any) // When allow_payloads is enabled, capture the raw request body into analytics metadata. @@ -132,7 +129,6 @@ func (a *AnalyticsPolicy) OnRequest(ctx *policy.RequestContext, params map[strin analyticsMetadata["request_payload"] = string(ctx.Body.Content) } - // Extract common analytics data from the request // Based on the API kind, collect the analytics data apiKind := ctx.SharedContext.APIKind @@ -147,7 +143,7 @@ func (a *AnalyticsPolicy) OnRequest(ctx *policy.RequestContext, params map[strin // Currently no data is collected case KindMCP: // Collect analytics data specific for MCP scenario from request - if ctx.Headers != nil && len(ctx.Headers.GetAll()) > 0 { + if ctx.Headers != nil && len(ctx.Headers.GetAll()) > 0 { // Need to get the mcp-session-id from headers sessionIDs := ctx.Headers.Get("mcp-session-id") if len(sessionIDs) > 0 { @@ -184,9 +180,9 @@ func (a *AnalyticsPolicy) OnRequest(ctx *policy.RequestContext, params map[strin // Populate client info clientInfo := McpClientInfo{ - RequestedProtocolVersion: extractStringFromJsonpath(mcpPayload,ProtocolVersionJsonPath), - Name: extractStringFromJsonpath(mcpPayload,ClientNameJsonPath), - Version: extractStringFromJsonpath(mcpPayload,ClientVersionJsonPath), + RequestedProtocolVersion: extractStringFromJsonpath(mcpPayload, ProtocolVersionJsonPath), + Name: extractStringFromJsonpath(mcpPayload, ClientNameJsonPath), + Version: extractStringFromJsonpath(mcpPayload, ClientVersionJsonPath), } // Only set ClientInfo pointer if at least one field is non-empty so that omitempty can exclude it from JSON if clientInfo.RequestedProtocolVersion != "" || clientInfo.Name != "" || clientInfo.Version != "" { @@ -243,15 +239,8 @@ func (p *AnalyticsPolicy) OnResponse(ctx *policy.ResponseContext, params map[str // Store tokenInfo in analytics metadata for publishing analyticsMetadata := make(map[string]any) - // Extract user ID from AuthContext if available (set by jwt-auth policy) - if ctx.SharedContext.AuthContext != nil { - if userID, ok := ctx.SharedContext.AuthContext[AuthContextKeyUserID]; ok && userID != "" { - analyticsMetadata[AuthContextKeyUserID] = userID - slog.Debug("Analytics system policy: User ID extracted from AuthContext", - "userID", userID, - ) - } - } + // Note: UserID from AuthContext is directly extracted by the policy engine's analytics filter + // No need to copy it to analyticsMetadata here // Based on the API kind, collect the analytics data apiKind := ctx.SharedContext.APIKind @@ -315,7 +304,7 @@ func (p *AnalyticsPolicy) OnResponse(ctx *policy.ResponseContext, params map[str } case KindMCP: // Collect the analytics data specific for MCP specific scenario - if ctx.ResponseHeaders != nil && len(ctx.ResponseHeaders.GetAll()) > 0 { + if ctx.ResponseHeaders != nil && len(ctx.ResponseHeaders.GetAll()) > 0 { if analyticsMetadata["mcp_session_id"] == nil { sessionIDs := ctx.ResponseHeaders.Get("mcp-session-id") if len(sessionIDs) > 0 { @@ -328,7 +317,7 @@ func (p *AnalyticsPolicy) OnResponse(ctx *policy.ResponseContext, params map[str if ctx != nil && ctx.ResponseBody != nil && len(ctx.ResponseBody.Content) > 0 { var mcpResponsePayload map[string]interface{} responseContent := ctx.ResponseBody.Content - + // Check if response is in SSE format by inspecting content-type or content structure isSSE := false if ctx.ResponseHeaders != nil { @@ -337,12 +326,12 @@ func (p *AnalyticsPolicy) OnResponse(ctx *policy.ResponseContext, params map[str isSSE = true } } - + // Also check content structure if header check didn't confirm SSE if !isSSE && (strings.HasPrefix(string(responseContent), "event:") || strings.Contains(string(responseContent), "\ndata:")) { isSSE = true } - + // Parse SSE format if detected if isSSE { jsonData, err := parseSSEResponse(responseContent) @@ -352,7 +341,7 @@ func (p *AnalyticsPolicy) OnResponse(ctx *policy.ResponseContext, params map[str responseContent = jsonData } } - + // Unmarshal the JSON (either from SSE data field or direct response) if err := json.Unmarshal(responseContent, &mcpResponsePayload); err != nil { slog.Error("Failed to unmarshal MCP response body for server info analytics", "error", err) @@ -362,12 +351,12 @@ func (p *AnalyticsPolicy) OnResponse(ctx *policy.ResponseContext, params map[str Name: extractStringFromJsonpath(mcpResponsePayload, ServerInfoNameJsonPath), Version: extractStringFromJsonpath(mcpResponsePayload, ServerInfoVersionJsonPath), } - + // Populate server info serverInfo := McpServerInfo{ ProtocolVersion: extractStringFromJsonpath(mcpResponsePayload, ServerProtocolVersionJsonPath), } - + // Only set ServerInfo pointer if at least one field is non-empty if serverInfoDetails.Name != "" || serverInfoDetails.Version != "" { serverInfo.ServerInfo = &serverInfoDetails diff --git a/sdk/gateway/policy/v1alpha/context.go b/sdk/gateway/policy/v1alpha/context.go index 0f019b58b..7b92f1182 100644 --- a/sdk/gateway/policy/v1alpha/context.go +++ b/sdk/gateway/policy/v1alpha/context.go @@ -15,6 +15,64 @@ type Body struct { Present bool } +// JWTAuthDetails holds fields specific to JWT/OAuth2 token-based auth. +type JWTAuthDetails struct { + // Subject is the "sub" claim from the token. + Subject string + + // Issuer is the token issuer ("iss" claim, IdP URL). + Issuer string + + // Audience is the intended audience ("aud" claim). Can be multiple values. + Audience []string + + // Claims holds additional token claims that don't fit the typed fields. + Claims map[string]string +} + +// APIKeyAuthDetails holds fields specific to API key authentication. +// Placeholder for future auth-type-specific fields. +type APIKeyAuthDetails struct { + // Todo: API key tier, rate limit info, etc. +} + +// BasicAuthDetails holds fields specific to basic authentication. +// Placeholder for future auth-type-specific fields. +type BasicAuthDetails struct { + // Todo: Basic auth specific metadata +} + +// AuthContext holds authentication data produced by auth policies (jwt, oauth2, apikey, basic-auth) +// and consumed by downstream policies (analytics, rate limiting, etc.). +type AuthContext struct { + // Authenticated indicates whether the request passed authentication. + Authenticated bool + + // AuthType identifies the mechanism that authenticated the request. + // Values: "jwt", "oauth2", "apikey", "basic", or empty if unauthenticated. + AuthType string + + // UserID is the user identifier extracted from the authentication source. + UserID string + + // AppID is the application identifier associated with the request. + // e.g., client_id for OAuth2, application owning the API key, etc. + AppID string + + // Scopes contains granted scopes as a set for O(1) lookup. + // Applicable for OAuth2/JWT and potentially API key auth. + Scopes map[string]bool + + // Auth-type-specific details. Only the relevant one is non-nil. + JWT *JWTAuthDetails + APIKey *APIKeyAuthDetails + Basic *BasicAuthDetails + + // Properties holds additional key-value data for inter-policy communication + // that does not fit the typed fields above. + Properties map[string]string +} + // SharedContext contains data shared across request and response phases type SharedContext struct { // ProjectID is the project ID which the API is associated with @@ -55,8 +113,8 @@ type SharedContext struct { OperationPath string // AuthContext stores authentication-related information - // Policies can read/write this map to share auth data (e.g., user ID) - AuthContext map[string]string + // Populated by auth policies, consumed by downstream policies + AuthContext *AuthContext } // RequestContext is mutable context for request phase containing current request state