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:44 UTC

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

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;
+    }
+}