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