{"record":{"id":"43f7760c93c70356","repo":"apache/druid","slug":"user-s-does-not-have-role-s","errorCode":null,"errorMessage":"User [%s] does not have role [%s].","messagePattern":"User \\[(.+?)\\] does not have role \\[(.+?)\\]\\.","errorType":"http","errorClass":"BasicSecurityDBResourceException","httpStatus":400,"severity":"error","filePath":"extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/db/updater/CoordinatorBasicAuthorizerMetadataStorageUpdater.java","lineNumber":1036,"sourceCode":"        objectMapper,\n        oldRoleMapValue\n    );\n    if (roleMap.get(roleName) == null) {\n      throw new BasicSecurityDBResourceException(\"Role [%s] does not exist.\", roleName);\n    }\n\n    byte[] oldUserMapValue = getCurrentUserMapBytes(prefix);\n    Map<String, BasicAuthorizerUser> userMap = BasicAuthUtils.deserializeAuthorizerUserMap(\n        objectMapper,\n        oldUserMapValue\n    );\n    BasicAuthorizerUser user = userMap.get(userName);\n    if (userMap.get(userName) == null) {\n      throw new BasicSecurityDBResourceException(\"User [%s] does not exist.\", userName);\n    }\n\n    if (!user.getRoles().contains(roleName)) {\n      throw new BasicSecurityDBResourceException(\"User [%s] does not have role [%s].\", userName, roleName);\n    }\n\n    user.getRoles().remove(roleName);\n    byte[] newUserMapValue = BasicAuthUtils.serializeAuthorizerUserMap(objectMapper, userMap);\n\n    // Role map is unchanged, but submit as an update to ensure that the table didn't change (e.g., role deleted)\n    return tryUpdateUserAndRoleMap(\n        prefix,\n        userMap, oldUserMapValue, newUserMapValue,\n        roleMap, oldRoleMapValue, oldRoleMapValue\n    );\n  }\n\n  private boolean assignGroupMappingRoleOnce(String prefix, String groupMappingName, String roleName)\n  {\n    byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix);\n    Map<String, BasicAuthorizerRole> roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(\n        objectMapper,","sourceCodeStart":1018,"sourceCodeEnd":1054,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/db/updater/CoordinatorBasicAuthorizerMetadataStorageUpdater.java#L1018-L1054","documentation":"Thrown by the coordinator basic authorizer metadata storage updater when unassigning a role from a user: the user exists in the authorizer's metadata store but its role set does not contain the requested role. Druid validates the user-role association before writing the updated user map, so the delete fails fast with BasicSecurityDBResourceException instead of silently no-oping.","triggerScenarios":"Calling the coordinator security API DELETE to unassign a role (unassignRoleFromUser path) with a (userName, roleName) pair where the user was never assigned that role; racing with another admin who already removed the role from the user; stale client-side state from a cached user listing.","commonSituations":"Automation scripts that revoke roles idempotently without first checking the user's current roles; cleanup tooling deleting role assignments that were already removed; concurrent admin operations through the Druid console and API.","solutions":["Verify the user's current roles via GET /druid-ext/basic-security/authorization/db/v1/<authorizerName>/users/<userName> before unassigning","Treat this 400 response as an idempotent success if the goal is 'user must not have role' and skip retrying","Re-fetch the user list to refresh stale caches before retrying","Check for concurrent admin/API callers modifying the same user"],"exampleFix":"// before (blind delete)\nclient.delete(\"/druid-ext/basic-security/authorization/db/v1/internal-auth/users/alice/roles/readonly\");\n// after (check first)\nBasicAuthorizerUser user = client.getUser(\"internal-auth\", \"alice\");\nif (user.getRoles().contains(\"readonly\")) {\n  client.delete(\"/druid-ext/basic-security/authorization/db/v1/internal-auth/users/alice/roles/readonly\");\n}","handlingStrategy":"try-catch","validationCode":"// check assignment before deleting\nSet<String> roles = getUser(authorizer, userName).getRoles();\nif (!roles.contains(roleName)) return; // nothing to revoke","typeGuard":"boolean isAssigned(BasicAuthorizerUser u, String role) { return u != null && u.getRoles() != null && u.getRoles().contains(role); }","tryCatchPattern":"try {\n  unassignRole(authorizer, userName, roleName);\n} catch (BasicSecurityDBResourceException e) {\n  if (e.getMessage().contains(\"does not have role\")) { /* already revoked: treat as success */ }\n  else throw e;\n}","preventionTips":["Read the user's roles before mutating them","Make revoke operations idempotent by treating 'does not have role' as success","Avoid concurrent admin edits to the same user","Use exact role names from a listing, not hand-typed strings"],"tags":["druid","basic-security","authorization","rest-api"],"backgroundTag":"resource-not-found","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"}