{"record":{"id":"6fcce67ba13f5a6b","repo":"apache/druid","slug":"group-mapping-s-already-has-role-s","errorCode":null,"errorMessage":"Group mapping [%s] already has role [%s].","messagePattern":"Group mapping \\[(.+?)\\] already has 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":1072,"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] already has role [%s].\", groupMappingName, roleName);\n    }\n\n    groupMapping.getRoles().add(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 unassignGroupMappingRoleOnce(String prefix, String groupMappingName, String roleName)\n  {\n    byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix);\n    Map<String, BasicAuthorizerRole> roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(\n        objectMapper,","sourceCodeStart":1054,"sourceCodeEnd":1090,"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#L1054-L1090","documentation":"Thrown when assigning a role to a group mapping that already contains that role. Druid treats the assignment as already satisfied and throws BasicSecurityDBResourceException instead of allowing a duplicate entry in the mapping's role set.","triggerScenarios":"POST to /groupMappings/<name>/roles/<role> when the mapping's roles list already includes the role; re-running a provisioning script without idempotency checks; two admins or jobs performing the same assignment concurrently.","commonSituations":"Idempotent infrastructure-as-code re-apply hitting an already-applied state; retry logic resending a request that actually succeeded the first time; duplicate entries in automation config applied in a loop.","solutions":["Check the mapping's current roles via GET .../groupMappings/<name> and skip the assignment if the role is present","Treat the 400 'already has role' response as success in idempotent provisioning flows","Deduplicate the role list in automation config before applying","Add locking/leader election if multiple jobs assign roles concurrently"],"exampleFix":"// before\nclient.post(\"/druid-ext/basic-security/authorization/db/v1/internal-auth/groupMappings/ldap-admins/roles/admin\");\n// after (idempotent)\nGroupMapping gm = client.getGroupMapping(\"internal-auth\", \"ldap-admins\");\nif (!gm.getRoles().contains(\"admin\")) {\n  client.post(\"/druid-ext/basic-security/authorization/db/v1/internal-auth/groupMappings/ldap-admins/roles/admin\");\n}","handlingStrategy":"try-catch","validationCode":"// skip assignment if already present\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  assignRoleToGroupMapping(authorizer, mappingName, roleName);\n} catch (BasicSecurityDBResourceException e) {\n  if (e.getMessage().contains(\"already has role\")) { /* idempotent success */ }\n  else throw e;\n}","preventionTips":["Design provisioning as idempotent: check-then-apply or tolerate 'already has role'","Deduplicate role lists in config before applying","Serialize concurrent role-assignment jobs for the same mapping","Fetch current mapping state instead of relying on cached listings"],"tags":["druid","basic-security","authorization","duplicate","rest-api"],"backgroundTag":"invalid-state-transition","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"}