You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by jk...@apache.org on 2021/03/31 17:30:43 UTC

[unomi] branch guestAuthenticationPublicRest created (now 5028808)

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

jkevan pushed a change to branch guestAuthenticationPublicRest
in repository https://gitbox.apache.org/repos/asf/unomi.git.


      at 5028808  UNOMI-453: provide authentication configuration on REST endpoints, also provide default implementation for unomi and the public REST Endpoints

This branch includes the following new commits:

     new 5028808  UNOMI-453: provide authentication configuration on REST endpoints, also provide default implementation for unomi and the public REST Endpoints

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[unomi] 01/01: UNOMI-453: provide authentication configuration on REST endpoints, also provide default implementation for unomi and the public REST Endpoints

Posted by jk...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jkevan pushed a commit to branch guestAuthenticationPublicRest
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit 502880866a7d97ae2432650da055e144798f3b28
Author: Kevan <ke...@jahia.com>
AuthorDate: Wed Mar 31 19:30:29 2021 +0200

    UNOMI-453: provide authentication configuration on REST endpoints, also provide default implementation for unomi and the public REST Endpoints
---
 package/src/main/resources/etc/users.properties    |  2 +-
 rest/pom.xml                                       |  6 ++
 .../java/org/apache/unomi/rest/RestServer.java     | 26 ++++--
 .../rest/authentication/AuthenticationFilter.java  | 99 ++++++++++++++++++++++
 .../authentication/AuthorizingInterceptor.java     | 50 +++++++++++
 .../authentication/RestAuthenticationConfig.java   | 53 ++++++++++++
 .../impl/DefaultRestAuthenticationConfig.java      | 60 +++++++++++++
 7 files changed, 290 insertions(+), 6 deletions(-)

diff --git a/package/src/main/resources/etc/users.properties b/package/src/main/resources/etc/users.properties
index 1c9cf58..3848b12 100644
--- a/package/src/main/resources/etc/users.properties
+++ b/package/src/main/resources/etc/users.properties
@@ -30,4 +30,4 @@
 # with the name "karaf".
 #
 karaf = ${org.apache.unomi.security.root.password:-karaf},_g_:admingroup
-_g_\:admingroup = group,admin,manager,viewer,systembundles,ssh
+_g_\:admingroup = group,admin,manager,viewer,systembundles,ssh,ROLE_UNOMI_ADMIN
diff --git a/rest/pom.xml b/rest/pom.xml
index f1133e3..7ed7909 100644
--- a/rest/pom.xml
+++ b/rest/pom.xml
@@ -133,6 +133,12 @@
             <groupId>commons-collections</groupId>
             <artifactId>commons-collections</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.karaf.jaas</groupId>
+            <artifactId>org.apache.karaf.jaas.boot</artifactId>
+            <version>${version.karaf}</version>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/rest/src/main/java/org/apache/unomi/rest/RestServer.java b/rest/src/main/java/org/apache/unomi/rest/RestServer.java
index 12c60ac..d5685b1 100644
--- a/rest/src/main/java/org/apache/unomi/rest/RestServer.java
+++ b/rest/src/main/java/org/apache/unomi/rest/RestServer.java
@@ -19,10 +19,15 @@ package org.apache.unomi.rest;
 import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
 import org.apache.cxf.Bus;
 import org.apache.cxf.endpoint.Server;
+import org.apache.cxf.interceptor.security.SimpleAuthorizingInterceptor;
 import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
 import org.apache.cxf.jaxrs.openapi.OpenApiCustomizer;
 import org.apache.cxf.jaxrs.openapi.OpenApiFeature;
 import org.apache.cxf.jaxrs.security.JAASAuthenticationFilter;
+import org.apache.cxf.jaxrs.security.SimpleAuthorizingFilter;
+import org.apache.unomi.rest.authentication.AuthenticationFilter;
+import org.apache.unomi.rest.authentication.AuthorizingInterceptor;
+import org.apache.unomi.rest.authentication.RestAuthenticationConfig;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.Filter;
 import org.osgi.framework.ServiceReference;
@@ -47,6 +52,7 @@ public class RestServer {
     private BundleContext bundleContext;
     private ServiceTracker jaxRSServiceTracker;
     private Bus serverBus;
+    private RestAuthenticationConfig restAuthenticationConfig;
     private List<ExceptionMapper> exceptionMappers = new ArrayList<>();
     private long timeOfLastUpdate = System.currentTimeMillis();
     private Timer refreshTimer = null;
@@ -61,6 +67,11 @@ public class RestServer {
         this.serverBus = serverBus;
     }
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    public void setRestAuthenticationConfig(RestAuthenticationConfig restAuthenticationConfig) {
+        this.restAuthenticationConfig = restAuthenticationConfig;
+    }
+
     @Reference
     public void addExceptionMapper(ExceptionMapper exceptionMapper) {
         this.exceptionMappers.add(exceptionMapper);
@@ -161,11 +172,16 @@ public class RestServer {
                         new org.apache.unomi.persistence.spi.CustomObjectMapper(),
                         JacksonJaxbJsonProvider.DEFAULT_ANNOTATIONS));
         jaxrsServerFactoryBean.setProvider(new org.apache.cxf.rs.security.cors.CrossOriginResourceSharingFilter());
-        JAASAuthenticationFilter jaasFilter = new org.apache.cxf.jaxrs.security.JAASAuthenticationFilter();
-        jaasFilter.setContextName("karaf");
-        jaasFilter.setRoleClassifier("ROLE_");
-        jaasFilter.setRealmName("cxs");
-        jaxrsServerFactoryBean.setProvider(jaasFilter);
+
+        // Authentication filter (used for authenticating user from request)
+        jaxrsServerFactoryBean.setProvider(new AuthenticationFilter(restAuthenticationConfig));
+
+        // Authorization interceptor (used for checking roles at methods access directly)
+        SimpleAuthorizingFilter simpleAuthorizingFilter = new SimpleAuthorizingFilter();
+        simpleAuthorizingFilter.setInterceptor(new AuthorizingInterceptor(restAuthenticationConfig));
+        jaxrsServerFactoryBean.setProvider(simpleAuthorizingFilter);
+
+        // Exception mappers
         for (ExceptionMapper exceptionMapper : exceptionMappers) {
             jaxrsServerFactoryBean.setProvider(exceptionMapper);
         }
diff --git a/rest/src/main/java/org/apache/unomi/rest/authentication/AuthenticationFilter.java b/rest/src/main/java/org/apache/unomi/rest/authentication/AuthenticationFilter.java
new file mode 100644
index 0000000..80fe76a
--- /dev/null
+++ b/rest/src/main/java/org/apache/unomi/rest/authentication/AuthenticationFilter.java
@@ -0,0 +1,99 @@
+/*
+ * 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.rest.authentication;
+
+import org.apache.cxf.interceptor.security.JAASLoginInterceptor;
+import org.apache.cxf.interceptor.security.RolePrefixSecurityContextImpl;
+import org.apache.cxf.jaxrs.security.JAASAuthenticationFilter;
+import org.apache.cxf.jaxrs.utils.JAXRSUtils;
+import org.apache.cxf.security.SecurityContext;
+import org.apache.karaf.jaas.boot.principal.RolePrincipal;
+import org.apache.karaf.jaas.boot.principal.UserPrincipal;
+
+import javax.annotation.Priority;
+import javax.security.auth.Subject;
+import javax.ws.rs.Priorities;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.container.PreMatching;
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * A wrapper filter around JAASAuthenticationFilter so that we can deactivate JAAS login around some resources and make
+ * them publicly accessible.
+ */
+@PreMatching
+@Priority(Priorities.AUTHENTICATION)
+public class AuthenticationFilter implements ContainerRequestFilter {
+
+    // Guest user config
+    public static final String GUEST_USERNAME = "guest";
+    public static final String GUEST_DEFAULT_ROLE = "ROLE_UNOMI_PUBLIC";
+    private static final List<String> GUEST_ROLES = Collections.singletonList(GUEST_DEFAULT_ROLE);
+    private static final Subject GUEST_SUBJECT = new Subject();
+    static {
+        GUEST_SUBJECT.getPrincipals().add(new UserPrincipal(GUEST_USERNAME));
+        for (String roleName : GUEST_ROLES) {
+            GUEST_SUBJECT.getPrincipals().add(new RolePrincipal(roleName));
+        }
+    }
+
+    // JAAS config
+    private static final String ROLE_CLASSIFIER = "ROLE_UNOMI";
+    private static final String ROLE_CLASSIFIER_TYPE = JAASLoginInterceptor.ROLE_CLASSIFIER_PREFIX;
+    private static final String REALM_NAME = "cxs";
+    private static final String CONTEXT_NAME = "karaf";
+
+    private final JAASAuthenticationFilter jaasAuthenticationFilter;
+    private final RestAuthenticationConfig restAuthenticationConfig;
+
+    public AuthenticationFilter(RestAuthenticationConfig restAuthenticationConfig) {
+        this.restAuthenticationConfig = restAuthenticationConfig;
+
+        // Build wrapped jaas filter
+        jaasAuthenticationFilter = new JAASAuthenticationFilter();
+        jaasAuthenticationFilter.setRoleClassifier(ROLE_CLASSIFIER);
+        jaasAuthenticationFilter.setRoleClassifierType(ROLE_CLASSIFIER_TYPE);
+        jaasAuthenticationFilter.setContextName(CONTEXT_NAME);
+        jaasAuthenticationFilter.setRealmName(REALM_NAME);
+    }
+
+    @Override
+    public void filter(ContainerRequestContext requestContext) throws IOException {
+        if (isPublicPath(requestContext)) {
+            JAXRSUtils.getCurrentMessage().put(SecurityContext.class,
+                    new RolePrefixSecurityContextImpl(GUEST_SUBJECT, ROLE_CLASSIFIER, ROLE_CLASSIFIER_TYPE));
+        } else{
+            jaasAuthenticationFilter.filter(requestContext);
+        }
+    }
+
+    private boolean isPublicPath(ContainerRequestContext requestContext) {
+        // First we do some quick checks to protect against malformed requests
+        // TODO should be handle by input validation ?
+        if (requestContext.getMethod() == null ||
+                requestContext.getMethod().length() > 10 ||
+                requestContext.getUriInfo().getPath() == null) {
+            return false;
+        }
+
+        // check if current path is matching any public path patterns
+        String currentPath = requestContext.getMethod() + " " + requestContext.getUriInfo().getPath();
+        return restAuthenticationConfig.getPublicPathPatterns().stream().anyMatch(pattern -> pattern.matcher(currentPath).matches());
+    }
+}
\ No newline at end of file
diff --git a/rest/src/main/java/org/apache/unomi/rest/authentication/AuthorizingInterceptor.java b/rest/src/main/java/org/apache/unomi/rest/authentication/AuthorizingInterceptor.java
new file mode 100644
index 0000000..fa2ab8d
--- /dev/null
+++ b/rest/src/main/java/org/apache/unomi/rest/authentication/AuthorizingInterceptor.java
@@ -0,0 +1,50 @@
+/*
+ * 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.rest.authentication;
+
+import org.apache.cxf.interceptor.security.SimpleAuthorizingInterceptor;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+/**
+ * Override of the SimpleAuthorizingInterceptor
+ * In charge of testing role on method access
+ * The override allow to define roles mapping based on Class.method instead of only method names.
+ */
+public class AuthorizingInterceptor extends SimpleAuthorizingInterceptor {
+
+    public AuthorizingInterceptor(RestAuthenticationConfig restAuthenticationConfig) {
+        super();
+        setGlobalRoles(restAuthenticationConfig.getGlobalRoles());
+        setMethodRolesMap(restAuthenticationConfig.getMethodRolesMap());
+    }
+
+    @Override
+    protected List<String> getExpectedRoles(Method method) {
+        // let super class calculate the roles to see if he is able to find something
+        List<String> roles =  super.getExpectedRoles(method);
+        if (roles == null || roles == globalRoles) {
+            // super class didnt find any specific roles for the method, let's try with our custom ClassName.MethodName lookup
+            roles = methodRolesMap.get(method.getDeclaringClass().getName() + "." + method.getName());
+        }
+        if (roles != null) {
+            return roles;
+        }
+        return globalRoles;
+    }
+}
diff --git a/rest/src/main/java/org/apache/unomi/rest/authentication/RestAuthenticationConfig.java b/rest/src/main/java/org/apache/unomi/rest/authentication/RestAuthenticationConfig.java
new file mode 100644
index 0000000..4fd26e8
--- /dev/null
+++ b/rest/src/main/java/org/apache/unomi/rest/authentication/RestAuthenticationConfig.java
@@ -0,0 +1,53 @@
+/*
+ * 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.rest.authentication;
+
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * This interface provide rest authentication configuration for the rest server.
+ */
+public interface RestAuthenticationConfig {
+    /**
+     *
+     * @return the list of public paths (expected format is: "HTTP_METHOD HTTP_PATH", like: "GET context.json")
+     */
+    List<Pattern> getPublicPathPatterns();
+
+    /**
+     * This is the roles mapped to endpoints
+     * By default all methods are protected by the global roles
+     * But you can define more granularity by providing roles for given endpoint methods
+     *
+     * Multiple format supported for the keys:
+     * - Method precise signature:      org.apache.unomi.api.ContextResponse getContextJSON(java.lang.Stringjava.lang.Longjava.lang.String)
+     * - Class name + method name:      org.apache.unomi.rest.ContextJsonEndpoint.getContextJSON
+     * - Method name only:              getContextJSON
+     *
+     * @return the list of role mappings <methodKey, roles separated by single white spaces>, like: <"getContextJSON", "ROLE1 ROLE2 ROLE3">
+     */
+    Map<String, String> getMethodRolesMap();
+
+    /**
+     * Define the global roles required for accessing endpoints methods, in case the method doesnt have specific required roles
+     * It will fallback on this global roles
+     * @return Global roles separated with single white spaces, like: "ROLE1 ROLE2 ROLE3"
+     */
+    String getGlobalRoles();
+}
diff --git a/rest/src/main/java/org/apache/unomi/rest/authentication/impl/DefaultRestAuthenticationConfig.java b/rest/src/main/java/org/apache/unomi/rest/authentication/impl/DefaultRestAuthenticationConfig.java
new file mode 100644
index 0000000..bc41bb9
--- /dev/null
+++ b/rest/src/main/java/org/apache/unomi/rest/authentication/impl/DefaultRestAuthenticationConfig.java
@@ -0,0 +1,60 @@
+/*
+ * 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.rest.authentication.impl;
+
+import org.apache.unomi.rest.authentication.RestAuthenticationConfig;
+import org.osgi.service.component.annotations.Component;
+
+import java.util.*;
+import java.util.regex.Pattern;
+
+/**
+ * Default implementation for the unomi authentication on Rest endpoints
+ */
+@Component(service = RestAuthenticationConfig.class)
+public class DefaultRestAuthenticationConfig implements RestAuthenticationConfig {
+
+    private static final String GUEST_ROLES = "ROLE_UNOMI_PUBLIC";
+    private static final String ADMIN_ROLES = "ROLE_UNOMI_ADMIN";
+
+    @Override
+    public List<Pattern> getPublicPathPatterns() {
+        List<Pattern> publicPaths = new ArrayList<>();
+        publicPaths.add(Pattern.compile("(GET|POST|OPTIONS) context\\.json"));
+        publicPaths.add(Pattern.compile("(GET|POST|OPTIONS) eventcollector"));
+        publicPaths.add(Pattern.compile("GET client/.*"));
+        return publicPaths;
+    }
+
+    @Override
+    public Map<String, String> getMethodRolesMap() {
+        Map<String, String> roleMappings = new HashMap<>();
+        roleMappings.put("org.apache.unomi.rest.ContextJsonEndpoint.contextJSONAsGet", GUEST_ROLES);
+        roleMappings.put("org.apache.unomi.rest.ContextJsonEndpoint.contextJSONAsPost", GUEST_ROLES);
+        roleMappings.put("org.apache.unomi.rest.ContextJsonEndpoint.options", GUEST_ROLES);
+        roleMappings.put("org.apache.unomi.rest.EventsCollectorEndpoint.collectAsGet", GUEST_ROLES);
+        roleMappings.put("org.apache.unomi.rest.EventsCollectorEndpoint.collectAsPost", GUEST_ROLES);
+        roleMappings.put("org.apache.unomi.rest.EventsCollectorEndpoint.options", GUEST_ROLES);
+        roleMappings.put("org.apache.unomi.rest.ClientEndpoint.getClient", GUEST_ROLES);
+        return roleMappings;
+    }
+
+    @Override
+    public String getGlobalRoles() {
+        return ADMIN_ROLES;
+    }
+}