UNOMI-393 Implement default field visibility provider that allows sending events and retrieving current profile (#405)

* UNOMI-393 Implement default field visibility provider that allows sending events and retrieving current profile

* UNOMI-393 Implement default field visibility provider that allows sending events and retrieving current profile

- make access to subscriptions protected
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/GraphQLServlet.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/GraphQLServlet.java
index 467f57f..93254f7 100644
--- a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/GraphQLServlet.java
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/GraphQLServlet.java
@@ -22,6 +22,7 @@
 import graphql.introspection.IntrospectionQuery;
 import org.apache.unomi.graphql.schema.GraphQLSchemaUpdater;
 import org.apache.unomi.graphql.services.ServiceManager;
+import org.apache.unomi.graphql.servlet.auth.GraphQLServletSecurityValidator;
 import org.apache.unomi.graphql.servlet.websocket.SubscriptionWebSocketFactory;
 import org.apache.unomi.graphql.utils.GraphQLObjectMapper;
 import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
@@ -46,10 +47,14 @@
 )
 public class GraphQLServlet extends WebSocketServlet {
 
+    public static final String SCHEMA_URL = "/schema.json";
+
     private GraphQLSchemaUpdater graphQLSchemaUpdater;
 
     private ServiceManager serviceManager;
 
+    private GraphQLServletSecurityValidator validator;
+
     @Reference
     public void setServiceManager(ServiceManager serviceManager) {
         this.serviceManager = serviceManager;
@@ -63,6 +68,7 @@
     @Override
     public void init(ServletConfig config) throws ServletException {
         super.init(config);
+        this.validator = new GraphQLServletSecurityValidator();
     }
 
     private WebSocketServletFactory factory;
@@ -95,7 +101,7 @@
     @Override
     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
         String query = req.getParameter("query");
-        if ("/schema.json".equals(req.getPathInfo())) {
+        if (SCHEMA_URL.equals(req.getPathInfo())) {
             query = IntrospectionQuery.INTROSPECTION_QUERY;
         }
         String operationName = req.getParameter("operationName");
@@ -107,6 +113,9 @@
             variables = GraphQLObjectMapper.getInstance().readValue(variableStr, typeRef);
         }
 
+        if (!validator.validate(query, operationName, req, resp)) {
+            return;
+        }
         setupCORSHeaders(req, resp);
         executeGraphQLRequest(resp, query, operationName, variables);
     }
@@ -127,6 +136,9 @@
             variables = new HashMap<>();
         }
 
+        if (!validator.validate(query, operationName, req, resp)) {
+            return;
+        }
         setupCORSHeaders(req, resp);
         executeGraphQLRequest(resp, query, operationName, variables);
     }
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/auth/GraphQLServletSecurityValidator.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/auth/GraphQLServletSecurityValidator.java
new file mode 100644
index 0000000..e3e36c8
--- /dev/null
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/auth/GraphQLServletSecurityValidator.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.unomi.graphql.servlet.auth;
+
+import graphql.language.Definition;
+import graphql.language.Document;
+import graphql.language.Field;
+import graphql.language.Node;
+import graphql.language.OperationDefinition;
+import graphql.parser.Parser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+
+import static graphql.language.OperationDefinition.Operation.MUTATION;
+import static graphql.language.OperationDefinition.Operation.QUERY;
+import static graphql.language.OperationDefinition.Operation.SUBSCRIPTION;
+import static org.osgi.service.http.HttpContext.AUTHENTICATION_TYPE;
+import static org.osgi.service.http.HttpContext.REMOTE_USER;
+
+public class GraphQLServletSecurityValidator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(GraphQLServletSecurityValidator.class);
+
+    private final Parser parser;
+
+    public GraphQLServletSecurityValidator() {
+        parser = new Parser();
+    }
+
+    public boolean validate(String query, String operationName, HttpServletRequest req, HttpServletResponse res) throws IOException {
+        if (isPublicOperation(query)) {
+            return true;
+        } else if (req.getHeader("Authorization") == null) {
+            res.addHeader("WWW-Authenticate", "Basic realm=\"karaf\"");
+            res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+            return false;
+        }
+
+        if (isAuthenticatedUser(req)) {
+            return true;
+        } else {
+            res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+            return false;
+        }
+    }
+
+    private boolean isPublicOperation(String query) {
+        final Document queryDoc = parser.parseDocument(query);
+        final Definition<?> def = queryDoc.getDefinitions().get(0);
+        if (def instanceof OperationDefinition) {
+            OperationDefinition opDef = (OperationDefinition) def;
+            if (SUBSCRIPTION.equals(opDef.getOperation())) {
+                // subscriptions are not public
+                return false;
+            } else if ("IntrospectionQuery".equals(opDef.getName())) {
+                // allow introspection query
+                return true;
+            }
+
+            List<Node> children = opDef.getSelectionSet().getChildren();
+            final Field cdp = (Field) children.stream().filter((node) -> {
+                return (node instanceof Field) && "cdp".equals(((Field) node).getName());
+            }).findFirst().orElse(null);
+            if (cdp == null) {
+                // allow not a cdp namespace
+                return true;
+            }
+
+            final List<String> allowedNodeNames = new ArrayList<>();
+            if (QUERY.equals(opDef.getOperation())) {
+                allowedNodeNames.add("getProfile");
+            } else if (MUTATION.equals(opDef.getOperation())) {
+                allowedNodeNames.add("processEvents");
+            }
+
+            return cdp.getSelectionSet().getChildren().stream().allMatch((node) -> {
+                return (node instanceof Field) && allowedNodeNames.contains(((Field) node).getName());
+            });
+        }
+        return true;
+    }
+
+    private boolean isAuthenticatedUser(HttpServletRequest req) {
+        req.setAttribute(AUTHENTICATION_TYPE, HttpServletRequest.BASIC_AUTH);
+
+        String authHeader = req.getHeader("Authorization");
+
+        String usernameAndPassword = new String(Base64.getDecoder().decode(authHeader.substring(6).getBytes()));
+        int userNameIndex = usernameAndPassword.indexOf(":");
+        String username = usernameAndPassword.substring(0, userNameIndex);
+        String password = usernameAndPassword.substring(userNameIndex + 1);
+
+        LoginContext loginContext;
+        try {
+            loginContext = new LoginContext("karaf", callbacks -> {
+                for (Callback callback : callbacks) {
+                    if (callback instanceof NameCallback) {
+                        ((NameCallback) callback).setName(username);
+                    } else if (callback instanceof PasswordCallback) {
+                        ((PasswordCallback) callback).setPassword(password.toCharArray());
+                    } else {
+                        throw new UnsupportedCallbackException(callback);
+                    }
+                }
+            });
+            loginContext.login();
+            Subject subject = loginContext.getSubject();
+            boolean success = subject != null;
+            if (success) {
+                req.setAttribute(REMOTE_USER, subject);
+            }
+            return success;
+        } catch (LoginException e) {
+            LOG.warn("Login failed", e);
+            return false;
+        }
+    }
+}
diff --git a/itests/src/test/java/org/apache/unomi/itests/graphql/BaseGraphQLIT.java b/itests/src/test/java/org/apache/unomi/itests/graphql/BaseGraphQLIT.java
index d2d76c3..9b8ff09 100644
--- a/itests/src/test/java/org/apache/unomi/itests/graphql/BaseGraphQLIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/graphql/BaseGraphQLIT.java
@@ -25,18 +25,16 @@
 import org.apache.http.util.EntityUtils;
 import org.apache.unomi.graphql.utils.GraphQLObjectMapper;
 import org.apache.unomi.itests.BaseIT;
-import org.apache.unomi.lifecycle.BundleWatcher;
-import org.junit.Before;
 import org.junit.runner.RunWith;
 import org.ops4j.pax.exam.junit.PaxExam;
 import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
 import org.ops4j.pax.exam.spi.reactors.PerSuite;
-import org.ops4j.pax.exam.util.Filter;
 import org.osgi.framework.BundleContext;
 
 import javax.inject.Inject;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Base64;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
@@ -53,13 +51,29 @@
     @Inject
     protected BundleContext bundleContext;
 
+    protected CloseableHttpResponse postAnonymous(final String resource) throws IOException {
+        return postAs(resource, null, null);
+    }
+
     protected CloseableHttpResponse post(final String resource) throws IOException {
+        return postAs(resource, "karaf", "karaf");
+    }
+
+    protected CloseableHttpResponse postAs(final String resource, final String username, final String password) throws IOException {
         final String resourceAsString = resourceAsString(resource);
 
         final HttpPost request = new HttpPost(GRAPHQL_ENDPOINT);
 
         request.setEntity(new StringEntity(resourceAsString, JSON_CONTENT_TYPE));
 
+        if (username != null && password != null) {
+            String basicAuth = username + ":" + password;
+            String wrap = "Basic " + new String(Base64.getEncoder().encode(basicAuth.getBytes()));
+            request.setHeader("Authorization", wrap);
+        } else {
+            request.removeHeaders("Authorization");
+        }
+
         return HttpClientBuilder.create().build().execute(request);
     }
 
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
new file mode 100644
index 0000000..2273b94
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLServletSecurityIT.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.itests.graphql;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class GraphQLServletSecurityIT extends BaseGraphQLIT {
+
+    @Test
+    public void testAnonymousProcessEventsRequest() throws Exception {
+        try (CloseableHttpResponse response = postAnonymous("graphql/security/process-events.json")) {
+            final ResponseContext context = ResponseContext.parse(response.getEntity());
+
+            Assert.assertEquals(200, response.getStatusLine().getStatusCode());
+            Assert.assertNotNull(context.getValue("data.cdp.processEvents"));
+        }
+    }
+
+    @Test
+    public void testAnonymousGetProfileRequest() throws Exception {
+        try (CloseableHttpResponse response = postAnonymous("graphql/security/get-profile.json")) {
+            final ResponseContext context = ResponseContext.parse(response.getEntity());
+
+            Assert.assertEquals(200, response.getStatusLine().getStatusCode());
+            Assert.assertNull(context.getValue("data.cdp.getProfile"));
+        }
+    }
+
+    @Test
+    public void testAnonymousGetSegmentRequest() throws Exception {
+        try (CloseableHttpResponse response = postAnonymous("graphql/security/get-segment.json")) {
+
+            Assert.assertEquals(401, response.getStatusLine().getStatusCode());
+        }
+    }
+
+    @Test
+    public void testAnonymousGetEventRequest() throws Exception {
+        try (CloseableHttpResponse response = postAnonymous("graphql/security/get-event.json")) {
+
+            Assert.assertEquals(401, response.getStatusLine().getStatusCode());
+        }
+    }
+
+    @Test
+    public void testAuthenticatedWrongGetEventRequest() throws Exception {
+        try (CloseableHttpResponse response = postAs("graphql/security/get-event.json", "karaf", "wrongPassword")) {
+
+            Assert.assertEquals(401, response.getStatusLine().getStatusCode());
+        }
+    }
+
+    @Test
+    public void testAuthenticatedGetEventRequest() throws Exception {
+        try (CloseableHttpResponse response = post("graphql/security/get-event.json")) {
+            final ResponseContext context = ResponseContext.parse(response.getEntity());
+
+            Assert.assertEquals(200, response.getStatusLine().getStatusCode());
+            Assert.assertNull(context.getValue("data.cdp.getEvent"));
+        }
+    }
+
+    @Test
+    public void testAnonymousSubscriptionRequest() throws Exception {
+        try (CloseableHttpResponse response = postAnonymous("graphql/security/subscribe.json")) {
+
+            Assert.assertEquals(401, response.getStatusLine().getStatusCode());
+        }
+    }
+}
diff --git a/itests/src/test/resources/graphql/security/get-event.json b/itests/src/test/resources/graphql/security/get-event.json
new file mode 100644
index 0000000..30a9d80
--- /dev/null
+++ b/itests/src/test/resources/graphql/security/get-event.json
@@ -0,0 +1,7 @@
+{
+  "operationName": "getEvent",
+  "variables": {
+    "id": "event-1"
+  },
+  "query": "query getEvent($id: String!) {\n  cdp {\n    getEvent(id: $id) {\n      id\n      cdp_profileID {\n        id\n      }\n    }\n  }\n}\n"
+}
diff --git a/itests/src/test/resources/graphql/security/get-profile.json b/itests/src/test/resources/graphql/security/get-profile.json
new file mode 100644
index 0000000..82409d0
--- /dev/null
+++ b/itests/src/test/resources/graphql/security/get-profile.json
@@ -0,0 +1,12 @@
+{
+  "operationName": "getProfile",
+  "variables": {
+    "profileID": {
+      "client": {
+        "id": "defaultClientId"
+      },
+      "id": "1234"
+    }
+  },
+  "query": "query getProfile($profileID: CDP_ProfileIDInput!) {\n  cdp {\n    getProfile(profileID: $profileID) {\n      firstName\n    }\n  }\n}\n"
+}
diff --git a/itests/src/test/resources/graphql/security/get-segment.json b/itests/src/test/resources/graphql/security/get-segment.json
new file mode 100644
index 0000000..074e014
--- /dev/null
+++ b/itests/src/test/resources/graphql/security/get-segment.json
@@ -0,0 +1,7 @@
+{
+  "operationName": "getSegment",
+  "variables": {
+    "segmentID": "testSegment"
+  },
+  "query": "query getSegment($segmentID: ID) {\n  cdp {\n    getSegment(segmentID: $segmentID) {\n      id\n      name\n      filter\n    }\n  }\n}\n"
+}
diff --git a/itests/src/test/resources/graphql/security/process-events.json b/itests/src/test/resources/graphql/security/process-events.json
new file mode 100644
index 0000000..a72c201
--- /dev/null
+++ b/itests/src/test/resources/graphql/security/process-events.json
@@ -0,0 +1,21 @@
+{
+  "operationName": "updateProfile",
+  "variables": {
+    "events": [
+      {
+        "cdp_objectID": "objectId",
+        "cdp_profileID": {
+          "id": "profile-1",
+          "client": {
+            "id": "defaultClientId"
+          }
+        },
+        "cdp_profileUpdateEvent": {
+          "firstName": "Gigi",
+          "lastName": "Bergkamp"
+        }
+      }
+    ]
+  },
+  "query": "mutation updateProfile($events: [CDP_EventInput]!) {\n  cdp {\n    processEvents(events: $events)\n  }\n}\n"
+}
diff --git a/itests/src/test/resources/graphql/security/subscribe.json b/itests/src/test/resources/graphql/security/subscribe.json
new file mode 100644
index 0000000..f57e61d
--- /dev/null
+++ b/itests/src/test/resources/graphql/security/subscribe.json
@@ -0,0 +1,5 @@
+{
+  "variables": {},
+  "operationName": "events",
+  "query": "subscription events($filter: CDP_EventFilterInput) {\n  eventListener(filter: $filter) {\n    id\n    cdp_profile {\n      firstName\n    }\n  }\n}\n"
+}