{"record":{"id":"584e08bdf6649d86","repo":"elastic/elasticsearch","slug":"entitlement-class-has-more-than-one-construct","errorCode":null,"errorMessage":"entitlement class [{}] has more than one constructor annotated with ExternalEntitlement","messagePattern":"entitlement class \\[(.+?)\\] has more than one constructor annotated with ExternalEntitlement","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java","lineNumber":229,"sourceCode":"        }\n    }\n\n    protected Entitlement parseEntitlement(String scopeName, String entitlementType) throws IOException {\n        XContentLocation startLocation = policyParser.getTokenLocation();\n        Class<?> entitlementClass = externalEntitlements.get(entitlementType);\n\n        if (entitlementClass == null) {\n            throw newPolicyParserException(scopeName, \"unknown entitlement type [\" + entitlementType + \"]\");\n        }\n\n        Constructor<?> entitlementConstructor = null;\n        Method entitlementMethod = null;\n        ExternalEntitlement entitlementMetadata = null;\n        for (var ctor : entitlementClass.getConstructors()) {\n            var metadata = ctor.getAnnotation(ExternalEntitlement.class);\n            if (metadata != null) {\n                if (entitlementMetadata != null) {\n                    throw new IllegalStateException(\n                        \"entitlement class [\"\n                            + entitlementClass.getName()\n                            + \"] has more than one constructor annotated with ExternalEntitlement\"\n                    );\n                }\n                entitlementConstructor = ctor;\n                entitlementMetadata = metadata;\n            }\n        }\n        for (var method : entitlementClass.getMethods()) {\n            var metadata = method.getAnnotation(ExternalEntitlement.class);\n            if (metadata != null) {\n                if (Modifier.isStatic(method.getModifiers()) == false) {\n                    throw new IllegalStateException(\n                        \"entitlement class [\" + entitlementClass.getName() + \"] has non-static method annotated with ExternalEntitlement\"\n                    );\n                }\n                if (entitlementMetadata != null) {","sourceCodeStart":211,"sourceCodeEnd":247,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java#L211-L247","documentation":"Thrown by PolicyParser while scanning an Entitlement class's constructors via reflection. The parser allows at most one constructor per class to carry @ExternalEntitlement; finding a second annotated constructor means the class declares two competing entry points and the parser cannot decide which to invoke when materialising the entitlement from a policy file.","triggerScenarios":"An Entitlement implementation class has two (or more) constructors, and both are annotated with @ExternalEntitlement. The error fires the first time PolicyParser.parseEntitlement resolves that class for any scope that references its entitlement type.","commonSituations":"Authoring a new Entitlement subclass and copy-pasting the @ExternalEntitlement annotation onto an overloaded constructor; refactoring a constructor into a builder-style multi-ctor setup without removing the annotation from the old one; merging two entitlement classes during a refactor.","solutions":["Inspect the named class and remove @ExternalEntitlement from all but one constructor.","If you genuinely need multiple construction shapes, route them through a single annotated static factory method instead and remove annotations from all constructors.","Rebuild and re-run the policy parse to confirm only one annotated constructor remains."],"exampleFix":"// before\npublic final class MyEntitlement implements Entitlement {\n    @ExternalEntitlement(parameterNames = {\"a\"})\n    public MyEntitlement(String a) { ... }\n\n    @ExternalEntitlement(parameterNames = {\"b\"})\n    public MyEntitlement(int b) { ... }\n}\n\n// after\npublic final class MyEntitlement implements Entitlement {\n    public MyEntitlement(String a) { ... }\n\n    @ExternalEntitlement(parameterNames = {\"a\"})\n    public static MyEntitlement build(String a) { return new MyEntitlement(a); }\n}","handlingStrategy":"validation","validationCode":"// At test time, assert each @ExternalEntitlement class has exactly one annotated member.\npublic static void assertSingleAnnotatedMember(Class<?> entitlementClass) {\n    int ctors = Arrays.stream(entitlementClass.getConstructors())\n        .filter(c -> c.isAnnotationPresent(ExternalEntitlement.class))\n        .toList().size();\n    int methods = Arrays.stream(entitlementClass.getDeclaredMethods())\n        .filter(m -> m.isAnnotationPresent(ExternalEntitlement.class))\n        .toList().size();\n    if (ctors + methods != 1) {\n        throw new AssertionError(entitlementClass.getName() + \" must have exactly one @ExternalEntitlement member, found \"\n            + ctors + \" ctor(s), \" + methods + \" method(s)\");\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Run a unit test that scans every Entitlement class for exactly one @ExternalEntitlement member.","Prefer static factory methods over annotated constructors to make the entry point obvious.","Add an architectural test (ArchUnit or similar) forbidding multiple annotations per class."],"tags":["entitlements","policy","annotation","reflection"],"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-12T06:17:24.410Z"}