This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch 
graphql-authorize-selected-operation
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit 4a41fd898752ad31ab70226c0899d3be4d187518
Author: Serge Huber <[email protected]>
AuthorDate: Fri Aug 28 07:33:00 2026 +0200

    Cover public-operation authorization with integration tests
    
    Adds a case for each way a caller holding only the public API key could try
    to have a privileged operation executed: naming an operation
    IntrospectionQuery, selecting a second operation by operationName, leading
    with a fragment definition, hiding a field in a fragment spread, leaving the
    executed operation ambiguous, and adding a root field beside an allowed one.
    
    These run end to end deliberately. The decision depends on how the real
    parser shapes a document and on what the servlet then executes, so a unit
    test over the classifier alone would not settle it. Each asserts that no
    privileged data comes back rather than only checking a status code, since a
    regression that returned data with a 200 would otherwise pass.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../itests/graphql/GraphQLServletSecurityIT.java   | 64 ++++++++++++++++++++++
 .../security/bypass-ambiguous-multi-operation.json |  4 ++
 .../graphql/security/bypass-extra-root-field.json  |  5 ++
 .../graphql/security/bypass-fragment-spread.json   |  5 ++
 .../security/bypass-introspection-named.json       |  5 ++
 .../graphql/security/bypass-leading-fragment.json  |  5 ++
 .../security/bypass-operation-name-smuggle.json    |  5 ++
 7 files changed, 93 insertions(+)

diff --git 
a/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLServletSecurityIT.java
 
b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLServletSecurityIT.java
index 4288ceaaf..6cb2e44aa 100644
--- 
a/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLServletSecurityIT.java
+++ 
b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLServletSecurityIT.java
@@ -84,4 +84,68 @@ public class GraphQLServletSecurityIT extends BaseGraphQLIT {
             Assert.assertEquals(401, response.getStatusLine().getStatusCode());
         }
     }
+
+    // 
---------------------------------------------------------------------------------------------
+    // Operation-authorization bypasses.
+    //
+    // Each payload below is a way a caller holding only the page-embedded 
public API key could try to
+    // get a privileged operation executed. The public allow-list is 
getProfile and processEvents only,
+    // so every one of these must be refused. They are written as end-to-end 
requests deliberately: the
+    // decision has to hold against the real parser and the real servlet, not 
just in a unit test.
+    // 
---------------------------------------------------------------------------------------------
+
+    /** An operation merely NAMED IntrospectionQuery must not be treated as 
introspection. */
+    @Test
+    public void 
testPublicKeyCannotRunPrivilegedOperationNamedIntrospectionQuery() throws 
Exception {
+        
assertRefusedForPublicKey("graphql/security/bypass-introspection-named.json");
+    }
+
+    /** A privileged operation selected by operationName, hidden behind a 
benign first operation. */
+    @Test
+    public void 
testPublicKeyCannotSmugglePrivilegedOperationViaOperationName() throws 
Exception {
+        
assertRefusedForPublicKey("graphql/security/bypass-operation-name-smuggle.json");
+    }
+
+    /** A leading fragment definition must not make the document look 
non-executable. */
+    @Test
+    public void testPublicKeyCannotBypassWithLeadingFragment() throws 
Exception {
+        
assertRefusedForPublicKey("graphql/security/bypass-leading-fragment.json");
+    }
+
+    /** A privileged field hidden inside a fragment spread must still be seen. 
*/
+    @Test
+    public void testPublicKeyCannotHidePrivilegedFieldInFragmentSpread() 
throws Exception {
+        
assertRefusedForPublicKey("graphql/security/bypass-fragment-spread.json");
+    }
+
+    /** Several operations and no operationName: which one executes is 
ambiguous, so it must be refused. */
+    @Test
+    public void testPublicKeyCannotUseAmbiguousMultiOperationDocument() throws 
Exception {
+        
assertRefusedForPublicKey("graphql/security/bypass-ambiguous-multi-operation.json");
+    }
+
+    /** An allowed field does not license a second root field alongside it. */
+    @Test
+    public void testPublicKeyCannotAddExtraRootFieldBesideAllowedOne() throws 
Exception {
+        
assertRefusedForPublicKey("graphql/security/bypass-extra-root-field.json");
+    }
+
+    /**
+     * A public-key caller must not get privileged data out of this document. 
Refusal is either a 401 or
+     * a 200 carrying no data — both are acceptable outcomes, what matters is 
that nothing privileged is
+     * returned. Asserting only on the status code would let a 200-with-data 
regression pass unnoticed.
+     */
+    private void assertRefusedForPublicKey(final String resource) throws 
Exception {
+        try (CloseableHttpResponse response = postWithAuthType(resource, 
AuthType.PUBLIC_KEY)) {
+            final int status = response.getStatusLine().getStatusCode();
+            if (status == 401) {
+                return;
+            }
+            Assert.assertEquals("Expected the request to be refused (401) or 
to return no privileged data",
+                    200, status);
+            final ResponseContext context = 
ResponseContext.parse(response.getEntity());
+            Assert.assertNull("A public API key must not be able to read 
profiles through " + resource,
+                    context.getValue("data.cdp.findProfiles"));
+        }
+    }
 }
diff --git 
a/itests/src/test/resources/graphql/security/bypass-ambiguous-multi-operation.json
 
b/itests/src/test/resources/graphql/security/bypass-ambiguous-multi-operation.json
new file mode 100644
index 000000000..6270eaece
--- /dev/null
+++ 
b/itests/src/test/resources/graphql/security/bypass-ambiguous-multi-operation.json
@@ -0,0 +1,4 @@
+{
+  "variables": {},
+  "query": "query Public {\n  cdp {\n    getProfile(profileID: {id: 
\"nobody\"}) {\n      cdp_profileIDs {\n        id\n      }\n    }\n  
}\n}\n\nquery Privileged {\n  cdp {\n    findProfiles(first: 100) {\n      
edges {\n        node {\n          cdp_profileIDs {\n            id\n          
}\n        }\n      }\n    }\n  }\n}\n"
+}
diff --git 
a/itests/src/test/resources/graphql/security/bypass-extra-root-field.json 
b/itests/src/test/resources/graphql/security/bypass-extra-root-field.json
new file mode 100644
index 000000000..b35f870db
--- /dev/null
+++ b/itests/src/test/resources/graphql/security/bypass-extra-root-field.json
@@ -0,0 +1,5 @@
+{
+  "operationName": "Mixed",
+  "variables": {},
+  "query": "query Mixed {\n  cdp {\n    getProfile(profileID: {id: 
\"nobody\"}) {\n      cdp_profileIDs {\n        id\n      }\n    }\n  }\n  
__schema {\n    types {\n      name\n    }\n  }\n}\n"
+}
diff --git 
a/itests/src/test/resources/graphql/security/bypass-fragment-spread.json 
b/itests/src/test/resources/graphql/security/bypass-fragment-spread.json
new file mode 100644
index 000000000..65297540b
--- /dev/null
+++ b/itests/src/test/resources/graphql/security/bypass-fragment-spread.json
@@ -0,0 +1,5 @@
+{
+  "operationName": "Privileged",
+  "variables": {},
+  "query": "query Privileged {\n  cdp {\n    ...privileged\n  }\n}\n\nfragment 
privileged on CDP_Query {\n  findProfiles(first: 100) {\n    edges {\n      
node {\n        cdp_profileIDs {\n          id\n        }\n      }\n    }\n  
}\n}\n"
+}
diff --git 
a/itests/src/test/resources/graphql/security/bypass-introspection-named.json 
b/itests/src/test/resources/graphql/security/bypass-introspection-named.json
new file mode 100644
index 000000000..09a8b50a6
--- /dev/null
+++ b/itests/src/test/resources/graphql/security/bypass-introspection-named.json
@@ -0,0 +1,5 @@
+{
+  "operationName": "IntrospectionQuery",
+  "variables": {},
+  "query": "query IntrospectionQuery {\n  cdp {\n    findProfiles(first: 100) 
{\n      edges {\n        node {\n          cdp_profileIDs {\n            id\n  
        }\n        }\n      }\n    }\n  }\n}\n"
+}
diff --git 
a/itests/src/test/resources/graphql/security/bypass-leading-fragment.json 
b/itests/src/test/resources/graphql/security/bypass-leading-fragment.json
new file mode 100644
index 000000000..91518038a
--- /dev/null
+++ b/itests/src/test/resources/graphql/security/bypass-leading-fragment.json
@@ -0,0 +1,5 @@
+{
+  "operationName": "Privileged",
+  "variables": {},
+  "query": "fragment ignored on CDP_Query {\n  __typename\n}\n\nquery 
Privileged {\n  cdp {\n    findProfiles(first: 100) {\n      edges {\n        
node {\n          cdp_profileIDs {\n            id\n          }\n        }\n    
  }\n    }\n  }\n}\n"
+}
diff --git 
a/itests/src/test/resources/graphql/security/bypass-operation-name-smuggle.json 
b/itests/src/test/resources/graphql/security/bypass-operation-name-smuggle.json
new file mode 100644
index 000000000..a3824c8ba
--- /dev/null
+++ 
b/itests/src/test/resources/graphql/security/bypass-operation-name-smuggle.json
@@ -0,0 +1,5 @@
+{
+  "operationName": "Privileged",
+  "variables": {},
+  "query": "query Public {\n  cdp {\n    getProfile(profileID: {id: 
\"nobody\"}) {\n      cdp_profileIDs {\n        id\n      }\n    }\n  
}\n}\n\nquery Privileged {\n  cdp {\n    findProfiles(first: 100) {\n      
edges {\n        node {\n          cdp_profileIDs {\n            id\n          
}\n        }\n      }\n    }\n  }\n}\n"
+}

Reply via email to