{"record":{"id":"9eba0c71cded3edb","repo":"apache/druid","slug":"unauthorized","errorCode":null,"errorMessage":"Unauthorized","messagePattern":"Unauthorized","errorType":"exception","errorClass":"ForbiddenException","httpStatus":403,"severity":"error","filePath":"extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java","lineNumber":184,"sourceCode":"    if (Strings.isNullOrEmpty(query.getId())) {\n      query = query.withId(UUID.randomUUID().toString());\n    }\n\n    final QueryLifecycle queryLifecycle = queryLifecycleFactory.factorize();\n\n    if (queryScheduler != null) {\n      final String queryId = query.getId();\n      cancelCallback.set(() -> queryScheduler.cancelQuery(queryId));\n    }\n\n    final org.apache.druid.server.QueryResponse queryResponse;\n    final String currThreadName = Thread.currentThread().getName();\n    Throwable caught = null;\n    try {\n      queryLifecycle.initialize(query);\n      AuthorizationResult authorizationResult = queryLifecycle.authorize(authResult);\n      if (!authorizationResult.allowAccessWithNoRestriction()) {\n        throw new ForbiddenException(Access.DEFAULT_ERROR_MESSAGE);\n      }\n      queryResponse = queryLifecycle.execute();\n\n      QueryToolChest queryToolChest = queryLifecycle.getToolChest();\n\n      Sequence<Object[]> sequence = queryToolChest.resultsAsArrays(query, queryResponse.getResults());\n      RowSignature rowSignature = queryToolChest.resultArraySignature(query);\n\n      Thread.currentThread().setName(StringUtils.format(\"grpc-native[%s]\", query.getId()));\n      final ByteString results = encodeNativeResults(request, sequence, rowSignature);\n      return QueryResponse.newBuilder()\n                          .setQueryId(query.getId())\n                          .setStatus(QueryStatus.OK)\n                          .setData(results)\n                          .clearErrorMessage()\n                          .addAllColumns(encodeNativeColumns(rowSignature, request.getSkipColumnsList()))\n                          .build();\n    }","sourceCodeStart":166,"sourceCodeEnd":202,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java#L166-L202","documentation":"In the gRPC query server's runNativeQuery, after the query is authorized via QueryLifecycle.authorize, any result that does not allow unrestricted access causes a ForbiddenException with Druid's default \"Unauthorized\" access message. The gRPC extension does not support resource-restricted (partial) authorization: if the authenticator/authorizer only grants restricted access to the requested resources, the whole query is rejected. It surfaces to the gRPC client as a runtime error with the message \"Unauthorized\".","triggerScenarios":"Calling the gRPC Query service with a native query whose authResult/authorizer yields AuthorizationResult that is not allowAccessWithNoRestriction() — e.g. the user lacks READ permission on the datasource(s) referenced by the query, or the authorizer returns access with restrictions (row/table filters) which gRPC cannot enforce.","commonSituations":"User credentials exist but the authorizer's role grants access only to other datasources; using an authorizer that returns restricted-access results (gRPC only accepts full access); stale or mis-scoped basic-auth credentials on the client; permissions changed after the client was configured.","solutions":["Grant the authenticated user/role READ (and any needed) permissions on all datasources the query touches via the Druid permissions UI or coordinator API.","Verify the authenticator/authorizer chain in the gRPC server config matches the credentials the client sends (basic auth is the supported path).","Check which resources the query references (datasource, metadata tables) and ensure each is authorized; one unauthorized resource rejects the entire query.","Capture the actual identity of the failing request (logs) to confirm the intended principal is being authenticated, not an anonymous/default one."],"exampleFix":"// before: user 'etl-bot' has no role with datasource access\n// grants via coordinator API after\nPOST /druid/coordinator/v1/authorizer/basic/roles/datasource-read/permissions\n[{\"resource\": {\"name\": \"wikipedia\", \"type\": \"DATASOURCE\"}, \"action\": \"READ\"}]\n// after: assign role\nPUT /druid/coordinator/v1/authorizer/basic/users/etl-bot/roles/datasource-read","handlingStrategy":"validation","validationCode":"// Client-side: confirm credentials and target datasource before calling\nif (username == null || password == null) throw new IllegalStateException(\"credentials required\");\n// ensure the principal has READ on every datasource in the query","typeGuard":null,"tryCatchPattern":"try {\n  grpcStub.query(request);\n} catch (StatusRuntimeException e) {\n  if (e.getStatus().getCode() == Status.Code.PERMISSION_DENIED || \"Unauthorized\".equals(e.getStatus().getDescription())) {\n    // fix grants / switch credentials\n  }\n  throw e;\n}","preventionTips":["Grant roles READ access to all datasources referenced by queries before deploying clients.","Test queries with the same credentials used in production via the Druid SQL CLI first.","Keep gRPC auth config aligned with the coordinator's authorizer setup.","Monitor for permission-denied responses after permission changes."],"tags":["grpc","authorization","forbidden","permissions"],"backgroundTag":"permission-denied","analyzedSha":"9b90983fd291f26935af934383ce360473179e4d","analyzedAt":"2026-09-07T13:32:30.957Z","contentChangedAt":"2026-09-07T13:32:30.957Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}