{"record":{"id":"dd05ff9f6c7a5fb8","repo":"apache/druid","slug":"group-mapping-s-does-not-have-role-s","errorCode":null,"errorMessage":"Group mapping [%s] does not have role [%s].","messagePattern":"Group mapping \\[(.+?)\\] 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":1108,"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[] oldGroupMappingMapValue = getCurrentGroupMappingMapBytes(prefix);\n    Map<String, BasicAuthorizerGroupMapping> groupMappingMap = BasicAuthUtils.deserializeAuthorizerGroupMappingMap(\n        objectMapper,\n        oldGroupMappingMapValue\n    );\n    BasicAuthorizerGroupMapping groupMapping = groupMappingMap.get(groupMappingName);\n    if (groupMappingMap.get(groupMappingName) == null) {\n      throw new BasicSecurityDBResourceException(\"Group mapping [%s] does not exist.\", groupMappingName);\n    }\n\n    if (!groupMapping.getRoles().contains(roleName)) {\n      throw new BasicSecurityDBResourceException(\"Group mapping [%s] does not have role [%s].\", groupMappingName, roleName);\n    }\n\n    groupMapping.getRoles().remove(roleName);\n    byte[] newGroupMapValue = BasicAuthUtils.serializeAuthorizerGroupMappingMap(objectMapper, groupMappingMap);\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 tryUpdateGroupMappingAndRoleMap(\n        prefix,\n        groupMappingMap, oldGroupMappingMapValue, newGroupMapValue,\n        roleMap, oldRoleMapValue, oldRoleMapValue\n    );\n  }\n\n  private boolean setPermissionsOnce(String prefix, String roleName, List<ResourceAction> permissions)\n  {\n    byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix);\n    Map<String, BasicAuthorizerRole> roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(\n        objectMapper,","sourceCodeStart":1090,"sourceCodeEnd":1126,"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#L1090-L1126","documentation":"Thrown when unassigning a role from a group mapping that does not currently hold that role. Druid validates membership before mutating the mapping and throws BasicSecurityDBResourceException rather than performing a no-op removal, since the subsequent metadata-store compare-and-swap would otherwise mask concurrent changes.","triggerScenarios":"DELETE to /groupMappings/<name>/roles/<role> when the mapping never had the role; another admin already removed the assignment; stale client state from an outdated group-mapping listing; re-running cleanup scripts.","commonSituations":"Idempotent revoke automation hitting an already-revoked state; concurrent admin edits via the Druid console and API; config reconciliation jobs assuming role assignments that were never made.","solutions":["Fetch the mapping (GET .../groupMappings/<name>) and only delete roles it actually contains","Treat this 400 as success in 'ensure role absent' idempotent flows","Refresh cached group-mapping state before retrying","Serialize role-management operations if multiple admins/tools mutate the same mapping"],"exampleFix":"// before (blind revoke)\nclient.delete(\"/druid-ext/basic-security/authorization/db/v1/internal-auth/groupMappings/ldap-admins/roles/auditor\");\n// after\nGroupMapping gm = client.getGroupMapping(\"internal-auth\", \"ldap-admins\");\nif (gm.getRoles().contains(\"auditor\")) {\n  client.delete(\"/druid-ext/basic-security/authorization/db/v1/internal-auth/groupMappings/ldap-admins/roles/auditor\");\n}","handlingStrategy":"try-catch","validationCode":"// only revoke roles the mapping actually has\nGroupMapping gm = getGroupMapping(authorizer, mappingName);\nif (gm == null || !gm.getRoles().contains(roleName)) return;","typeGuard":"boolean hasRole(GroupMapping m, String role) { return m != null && m.getRoles() != null && m.getRoles().contains(role); }","tryCatchPattern":"try {\n  unassignRoleFromGroupMapping(authorizer, mappingName, roleName);\n} catch (BasicSecurityDBResourceException e) {\n  if (e.getMessage().contains(\"does not have role\")) { /* already revoked: success */ }\n  else throw e;\n}","preventionTips":["Fetch the mapping's current roles before revoking","Make revoke flows idempotent (ignore 'does not have role')","Refresh client state after other admins' changes","Use one management path (console or API) at a time for the same mapping"],"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"}