{"record":{"id":"9860f647804534be","repo":"jaegertracing/jaeger","slug":"access-denied","errorCode":null,"errorMessage":"access denied","messagePattern":"access denied","errorType":"http","errorClass":null,"httpStatus":403,"severity":"warning","filePath":"components/extension/jaegerquery/queryinterceptor/interceptor.go","lineNumber":48,"sourceCode":"// storage protocol carry, so a change to the AST is a change to this contract — which is why\n// the AST lives in a public, versioned module.\npackage queryinterceptor\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n\n\t\"go.opentelemetry.io/collector/pdata/ptrace\"\n\n\texpression \"github.com/jaegertracing/jaeger-idl/query/expression/v1\"\n)\n\n// ErrAccessDenied is the sentinel that interceptor implementations wrap\n// when the caller's query is refused on access-control grounds. The API\n// layers map it to HTTP 403 / gRPC PERMISSION_DENIED instead of a\n// generic server error.\nvar ErrAccessDenied = errors.New(\"access denied\")\n\n// Query is the public view of a trace-search query passed to Interceptor.OnQuery.\n//\n// EXPERIMENTAL: this type and the Interceptor contract it belongs to may change or be removed\n// in any release, without a deprecation period. Filter in particular is an RFC 0005 filter AST,\n// which that RFC is still moving through its milestones, so an implementation should expect to\n// be updated alongside jaeger-query rather than to keep compiling against a stable shape.\n//\n// Every predicate is in Filter, including the ones a caller sent as the older scalar search\n// fields: jaeger-query expresses a service, an operation name, a tag and a duration bound as\n// filter predicates before an interceptor sees them, so an implementation reads and rewrites\n// one thing rather than a filter plus four fields that can say the same in two ways. The\n// remaining fields are the envelope, which no predicate lives in.\ntype Query struct {\n\t// Filter is the query's predicates as a boolean-valued expression (RFC 0005 §6), or nil\n\t// when the search asks for a time range and nothing else. Nil rather than an empty\n\t// conjunction, because `and` takes two arguments or more, so there is no expression that\n\t// says \"match everything\".","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/jaegertracing/jaeger/blob/806f4447841ecdb60519f408b004a599d515f437/components/extension/jaegerquery/queryinterceptor/interceptor.go#L30-L66","documentation":"ErrAccessDenied is the public sentinel that query interceptor implementations wrap when a trace query is refused on access-control grounds. The HTTP gateway and gRPC layers detect errors.Is(err, ErrAccessDenied) and map it to HTTP 403 / gRPC PERMISSION_DENIED instead of a generic 500.","triggerScenarios":"An Interceptor.OnQuery (or OnGetTrace etc.) implementation returns an error wrapping ErrAccessDenied via fmt.Errorf(\"...: %w\", queryinterceptor.ErrAccessDenied); tryHandleError then converts it to a 403 status response.","commonSituations":"RBAC/tenant filtering middleware rejecting queries for traces outside the caller's allowed scope; security policies denying access to specific operations; tests asserting the sentinel mapping via TestAsStatusError / TestHTTPGatewayTryHandleError.","solutions":["If the denial is unexpected, inspect the configured query interceptor implementation and its access-control rules to see why the caller was refused","If you implement an interceptor, always wrap your refusal error with ErrAccessDenied (`fmt.Errorf(\"trace not allowed for tenant %s: %w\", t, ErrAccessDenied)`) so clients get 403, not 500","Use errors.Is(err, queryinterceptor.ErrAccessDenied) in callers/tests to classify the failure rather than string matching"],"exampleFix":"// before\nreturn nil, fmt.Errorf(\"access to trace %s denied for tenant %s\", traceID, tenant)\n// after\nreturn nil, fmt.Errorf(\"access to trace %s denied for tenant %s: %w\", traceID, tenant, queryinterceptor.ErrAccessDenied)","handlingStrategy":"type-guard","validationCode":"// in interceptor implementation, ensure refusal errors wrap the sentinel\nif !allowed(req) {\n    return fmt.Errorf(\"query refused: %w\", queryinterceptor.ErrAccessDenied)\n}","typeGuard":"func isAccessDenied(err error) bool {\n    return errors.Is(err, queryinterceptor.ErrAccessDenied)\n}","tryCatchPattern":"resp, err := queryService.FindTraces(ctx, req)\nif err != nil {\n    if errors.Is(err, queryinterceptor.ErrAccessDenied) {\n        return status.Error(codes.PermissionDenied, \"access denied\")\n    }\n    return status.Error(codes.Internal, err.Error())\n}","preventionTips":["Always wrap denial errors with ErrAccessDenied via %w","Never swallow or re-wrap with a new error type losing the sentinel","Test interceptor refusals with errors.Is assertions"],"tags":["go","authorization","grpc","http","query"],"backgroundTag":"access-denied","analyzedSha":"806f4447841ecdb60519f408b004a599d515f437","analyzedAt":"2026-09-01T02:39:22.140Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}