{"record":{"id":"51190cac95e693cc","repo":"apache/druid","slug":"different-restrictions-on-table-s-previous-pol","errorCode":null,"errorMessage":"Different restrictions on table [%s]: previous policy [%s] and new policy [%s]","messagePattern":"Different restrictions on table \\[(.+?)\\]: previous policy \\[(.+?)\\] and new policy \\[(.+?)\\]","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"critical","filePath":"processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java","lineNumber":153,"sourceCode":"  {\n    return policy.createSegmentPruner();\n  }\n\n  @Override\n  public DataSource withPolicies(Map<String, Optional<Policy>> policyMap, PolicyEnforcer policyEnforcer)\n  {\n    if (!policyMap.containsKey(base.getName())) {\n      throw new ISE(\"Missing policy check result for table [%s]\", base.getName());\n    }\n\n    Optional<Policy> newPolicy = policyMap.getOrDefault(base.getName(), Optional.empty());\n    if (newPolicy.isEmpty() || newPolicy.get() instanceof NoRestrictionPolicy) {\n      // allow empty policy, which means no restriction.\n      // druid-internal calls with NoRestrictionPolicy: allow\n    } else if (newPolicy.get().equals(policy)) {\n      // same policy: allow\n    } else {\n      throw new ISE(\n          \"Different restrictions on table [%s]: previous policy [%s] and new policy [%s]\",\n          base.getName(),\n          policy,\n          newPolicy.get()\n      );\n    }\n    // The only happy path is, newPolicy is NoRestrictionPolicy, which means this comes from an anthenticated and\n    // authorized druid-internal request.\n    policyEnforcer.validateOrElseThrow(base, policy);\n    return this;\n  }\n\n  @Override\n  public String toString()\n  {\n    return \"RestrictedDataSource{\" +\n           \"base=\" + base +\n           \", policy=\" + policy + \"}\";","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java#L135-L171","documentation":"A RestrictedDataSource already carries a policy; re-running withPolicies must produce the identical policy (or an empty/NoRestrictionPolicy). If the newly resolved policy differs from the existing one, the two query-planning passes disagree about restrictions, so Druid throws ISE to prevent weakening row-level security mid-flight.","triggerScenarios":"Calling withPolicies with a policyMap whose entry for the table is a Policy that is neither empty/NoRestrictionPolicy nor equal to the existing policy; non-deterministic PolicyEnforcer returning different policies between the initial planning and re-check pass.","commonSituations":"Policy objects not implementing equals() consistently (custom Policy classes); policy metadata changing between query submission and execution; different enforcer instances configured differently across cluster nodes.","solutions":["Ensure the Policy implementation overrides equals()/hashCode() so logically identical policies compare equal","Make the PolicyEnforcer deterministic for the same table and credentials","Investigate why policy metadata differs between the two evaluation passes (stale cache, config drift)","If the restriction legitimately changed, re-plan the query from scratch rather than re-checking"],"exampleFix":"// before\nclass TenantPolicy implements Policy { private final String tenant; /* no equals */ }\n// after\n@Override\npublic boolean equals(Object o) {\n  return o instanceof TenantPolicy && ((TenantPolicy) o).tenant.equals(tenant);\n}\n@Override\npublic int hashCode() { return tenant.hashCode(); }","handlingStrategy":"try-catch","validationCode":"Optional<Policy> next = policyMap.getOrDefault(restrictedDs.getName(), Optional.empty());\nif (next.isPresent() && !(next.get() instanceof NoRestrictionPolicy) && !next.get().equals(currentPolicy)) {\n  throw new IllegalStateException(\"Policy drift between planning passes for \" + restrictedDs.getName());\n}","typeGuard":"boolean policiesCompatible(Policy a, Optional<Policy> b) {\n  return b.isEmpty() || b.get() instanceof NoRestrictionPolicy || b.get().equals(a);\n}","tryCatchPattern":"try {\n  DataSource ds = restricted.withPolicies(policyMap, enforcer);\n} catch (IllegalStateException e) {\n  // abort query; policy metadata changed mid-flight — re-plan\n}","preventionTips":["Implement equals()/hashCode() on all custom Policy classes","Keep policy metadata stable across the lifetime of a query","Use one deterministic enforcer configuration cluster-wide"],"tags":["druid","policy","security","datasource"],"backgroundTag":"internal-invariant-violation","analyzedSha":"9b90983fd291f26935af934383ce360473179e4d","analyzedAt":"2026-09-07T13:32:30.957Z","contentChangedAt":"2026-09-07T13:32:30.957Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}