diff --git a/gateway-provider-security-shiro/pom.xml b/gateway-provider-security-shiro/pom.xml
index 004a62370e..fb380c2e61 100644
--- a/gateway-provider-security-shiro/pom.xml
+++ b/gateway-provider-security-shiro/pom.xml
@@ -45,6 +45,10 @@
org.apache.knox
gateway-util-common
+
+ org.apache.knox
+ gateway-util-urltemplate
+
org.jboss.shrinkwrap
@@ -87,6 +91,11 @@
commons-io
+
+ org.apache.commons
+ commons-lang3
+
+
javax.servlet
javax.servlet-api
diff --git a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/ShiroMessages.java b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/ShiroMessages.java
new file mode 100644
index 0000000000..5a51664bf3
--- /dev/null
+++ b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/ShiroMessages.java
@@ -0,0 +1,31 @@
+/*
+ * 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.knox.gateway;
+
+import org.apache.knox.gateway.i18n.messages.Message;
+import org.apache.knox.gateway.i18n.messages.MessageLevel;
+import org.apache.knox.gateway.i18n.messages.Messages;
+
+@Messages(logger="org.apache.knox.gateway")
+public interface ShiroMessages {
+ @Message(level = MessageLevel.INFO, text = "Request {0} matches unauthenticated path configured in topology, letting it through" )
+ void unauthenticatedPathBypass(String uri);
+
+ @Message( level = MessageLevel.WARN, text = "Invalid URL pattern for rule: {0}" )
+ void invalidURLPattern(String rule);
+}
diff --git a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/deploy/impl/ShiroDeploymentContributor.java b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/deploy/impl/ShiroDeploymentContributor.java
index 345727f0df..61b1a4f140 100644
--- a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/deploy/impl/ShiroDeploymentContributor.java
+++ b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/deploy/impl/ShiroDeploymentContributor.java
@@ -32,6 +32,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
public class ShiroDeploymentContributor extends ProviderDeploymentContributorBase {
@@ -43,8 +44,10 @@ public class ShiroDeploymentContributor extends ProviderDeploymentContributorBas
private static final String SESSION_TIMEOUT = "sessionTimeout";
private static final String REMEMBER_ME = "rememberme";
private static final String SHRIO_CONFIG_FILE_NAME = "shiro.ini";
+ private static final String SHIRO_URL_CONFIG = "urls";
private static final int DEFAULT_SESSION_TIMEOUT = 30; // 30min
+
@Override
public String getRole() {
return "authentication";
@@ -132,6 +135,17 @@ public void contributeFilter( DeploymentContext context, Provider provider,
resource.addFilter().name( getName() ).role(
getRole() ).impl( SHIRO_FILTER_CLASSNAME ).params( params );
+
+ /* Collect all shiro `urls.` params and them as filter params to POST_FILTER_CLASSNAME */
+ Map shiroURLs = providerParams.entrySet().stream().filter(e -> e.getKey().startsWith(SHIRO_URL_CONFIG)).collect(
+ Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+ for(final Map.Entry m: shiroURLs.entrySet()) {
+ params.add( resource.createFilterParam()
+ .name( m.getKey() )
+ .value( m.getValue() ) );
+ }
+
resource.addFilter().name( "Post" + getName() ).role(
getRole() ).impl( POST_FILTER_CLASSNAME ).params( params );
}
diff --git a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/ShiroSubjectIdentityAdapter.java b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/ShiroSubjectIdentityAdapter.java
index 4fb945791b..0f8ef22b0f 100644
--- a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/ShiroSubjectIdentityAdapter.java
+++ b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/ShiroSubjectIdentityAdapter.java
@@ -17,21 +17,8 @@
*/
package org.apache.knox.gateway.filter;
-import java.io.IOException;
-import java.security.Principal;
-import java.security.PrivilegedExceptionAction;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.Callable;
-
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-
+import org.apache.commons.lang3.StringUtils;
+import org.apache.knox.gateway.ShiroMessages;
import org.apache.knox.gateway.audit.api.Action;
import org.apache.knox.gateway.audit.api.ActionOutcome;
import org.apache.knox.gateway.audit.api.AuditContext;
@@ -40,20 +27,76 @@
import org.apache.knox.gateway.audit.api.Auditor;
import org.apache.knox.gateway.audit.api.ResourceType;
import org.apache.knox.gateway.audit.log4j.audit.AuditConstants;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.security.GroupPrincipal;
import org.apache.knox.gateway.security.PrimaryPrincipal;
+import org.apache.knox.gateway.util.urltemplate.Matcher;
+import org.apache.knox.gateway.util.urltemplate.Parser;
+import org.apache.knox.gateway.util.urltemplate.Template;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.security.Principal;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
public class ShiroSubjectIdentityAdapter implements Filter {
+ private static final ShiroMessages LOG = MessagesFactory.get(ShiroMessages.class);
private static final String SUBJECT_USER_GROUPS = "subject.userGroups";
private static AuditService auditService = AuditServiceFactory.getAuditService();
private static Auditor auditor = auditService.getAuditor(
AuditConstants.DEFAULT_AUDITOR_NAME, AuditConstants.KNOX_SERVICE_NAME,
AuditConstants.KNOX_COMPONENT_NAME );
+ private static final String SHIRO_URL_CONFIG = "urls";
+ /* Map of shiro url configs */
+ private static Map urls = new HashMap<>();
+
+ /* List of URLs with anon authentication */
+ private static List anonUrls = new ArrayList<>();
@Override
public void init( FilterConfig filterConfig ) throws ServletException {
+ /* Create a shiro urls config map */
+ final Enumeration params = filterConfig.getInitParameterNames();
+ while (params.hasMoreElements()) {
+ String param = params.nextElement();
+ if (StringUtils.startsWithIgnoreCase(param, SHIRO_URL_CONFIG)) {
+ String value = filterConfig.getInitParameter(param);
+ final String pathParam = param.substring(param.indexOf('.') + 1);
+ urls.put(pathParam, value);
+ if("anon".equalsIgnoreCase(value)) {
+ final Template urlPatternTemplate;
+ final Matcher urlMatcher = new Matcher();
+ try {
+ urlPatternTemplate = Parser.parseTemplate(pathParam);
+ urlMatcher.add(urlPatternTemplate, pathParam);
+ anonUrls.add(urlMatcher);
+ } catch (URISyntaxException e) {
+ LOG.invalidURLPattern(param);
+ throw new ServletException(e);
+ }
+
+ }
+
+ }
+ }
}
@Override
@@ -96,56 +139,110 @@ public Void run() throws Exception {
};
Subject shiroSubject = SecurityUtils.getSubject();
+ /**
+ * For cases when we want anonymous authentication to urls in shiro.
+ * This is when do not want authentication for jwks endpoints using
+ * shiro.
+ */
if (shiroSubject == null || shiroSubject.getPrincipal() == null) {
- throw new IllegalStateException("Unable to determine authenticated user from Shiro, please check that your Knox Shiro configuration is correct");
- }
- final String principal = shiroSubject.getPrincipal().toString();
- Set principals = new HashSet<>();
- Principal p = new PrimaryPrincipal(principal);
- principals.add(p);
- AuditContext context = auditService.getContext();
- context.setUsername( principal );
- auditService.attachContext(context);
- String sourceUri = (String)request.getAttribute( AbstractGatewayFilter.SOURCE_REQUEST_CONTEXT_URL_ATTRIBUTE_NAME );
- auditor.audit( Action.AUTHENTICATION , sourceUri, ResourceType.URI, ActionOutcome.SUCCESS );
-
- Set userGroups;
- // map ldap groups saved in session to Java Subject GroupPrincipal(s)
- if (SecurityUtils.getSubject().getSession().getAttribute(SUBJECT_USER_GROUPS) != null) {
- userGroups = (Set)SecurityUtils.getSubject().getSession().getAttribute(SUBJECT_USER_GROUPS);
- } else { // KnoxLdapRealm case
- if( shiroSubject.getPrincipal() instanceof String ) {
- userGroups = new HashSet<>(shiroSubject.getPrincipals().asSet());
- userGroups.remove(principal);
- } else { // KnoxPamRealm case
- Set shiroPrincipals = new HashSet<>(shiroSubject.getPrincipals().asSet());
- userGroups = new HashSet<>(); // Here we are creating a new UserGroup
- // so we don't need to remove a Principal
- // In the case of LDAP the userGroup may have already Principal
- // added to the list of groups, so it is not needed.
- for( Principal shiroPrincipal: shiroPrincipals ) {
- userGroups.add(shiroPrincipal.toString() );
- }
+ if(!isRequestPathInShiroConfig(((HttpServletRequest)request))) {
+ throw new IllegalStateException("Unable to determine authenticated user from Shiro, please check that your Knox Shiro configuration is correct");
}
+
+ LOG.unauthenticatedPathBypass(
+ ((HttpServletRequest) request).getRequestURI());
+ final String principal = "anonymous";
+ javax.security.auth.Subject subject = new javax.security.auth.Subject();
+ subject.getPrincipals().add(new PrimaryPrincipal(principal));
+ AuditContext context = auditService.getContext();
+ context.setUsername(principal);
+ auditService.attachContext(context);
+ javax.security.auth.Subject.doAs(subject, action);
+ } else {
+
+ final String principal = shiroSubject.getPrincipal().toString();
+ Set principals = new HashSet<>();
+ Principal p = new PrimaryPrincipal(principal);
+ principals.add(p);
+ AuditContext context = auditService.getContext();
+ context.setUsername(principal);
+ auditService.attachContext(context);
+ String sourceUri = (String) request.getAttribute(
+ AbstractGatewayFilter.SOURCE_REQUEST_CONTEXT_URL_ATTRIBUTE_NAME);
+ auditor.audit(Action.AUTHENTICATION, sourceUri, ResourceType.URI,
+ ActionOutcome.SUCCESS);
+
+ Set userGroups;
+ // map ldap groups saved in session to Java Subject GroupPrincipal(s)
+ if (SecurityUtils.getSubject().getSession()
+ .getAttribute(SUBJECT_USER_GROUPS) != null) {
+ userGroups = (Set) SecurityUtils.getSubject().getSession()
+ .getAttribute(SUBJECT_USER_GROUPS);
+ } else { // KnoxLdapRealm case
+ if (shiroSubject.getPrincipal() instanceof String) {
+ userGroups = new HashSet<>(shiroSubject.getPrincipals().asSet());
+ userGroups.remove(principal);
+ } else { // KnoxPamRealm case
+ Set shiroPrincipals = new HashSet<>(
+ shiroSubject.getPrincipals().asSet());
+ userGroups = new HashSet<>(); // Here we are creating a new UserGroup
+ // so we don't need to remove a Principal
+ // In the case of LDAP the userGroup may have already Principal
+ // added to the list of groups, so it is not needed.
+ for (Principal shiroPrincipal : shiroPrincipals) {
+ userGroups.add(shiroPrincipal.toString());
+ }
+ }
+ }
+ for (String userGroup : userGroups) {
+ Principal gp = new GroupPrincipal(userGroup);
+ principals.add(gp);
+ }
+ auditor.audit(Action.AUTHENTICATION, sourceUri, ResourceType.URI,
+ ActionOutcome.SUCCESS, "Groups: " + userGroups);
+
+ // The newly constructed Sets check whether this Subject has been set read-only
+ // before permitting subsequent modifications. The newly created Sets also prevent
+ // illegal modifications by ensuring that callers have sufficient permissions.
+ //
+ // To modify the Principals Set, the caller must have AuthPermission("modifyPrincipals").
+ // To modify the public credential Set, the caller must have AuthPermission("modifyPublicCredentials").
+ // To modify the private credential Set, the caller must have AuthPermission("modifyPrivateCredentials").
+ javax.security.auth.Subject subject = new javax.security.auth.Subject(
+ true, principals, Collections.emptySet(), Collections.emptySet());
+ javax.security.auth.Subject.doAs(subject, action);
}
- for (String userGroup : userGroups) {
- Principal gp = new GroupPrincipal(userGroup);
- principals.add(gp);
- }
- auditor.audit( Action.AUTHENTICATION , sourceUri, ResourceType.URI, ActionOutcome.SUCCESS, "Groups: " + userGroups );
-
- // The newly constructed Sets check whether this Subject has been set read-only
- // before permitting subsequent modifications. The newly created Sets also prevent
- // illegal modifications by ensuring that callers have sufficient permissions.
- //
- // To modify the Principals Set, the caller must have AuthPermission("modifyPrincipals").
- // To modify the public credential Set, the caller must have AuthPermission("modifyPublicCredentials").
- // To modify the private credential Set, the caller must have AuthPermission("modifyPrivateCredentials").
- javax.security.auth.Subject subject = new javax.security.auth.Subject(true, principals, Collections.emptySet(), Collections.emptySet());
- javax.security.auth.Subject.doAs( subject, action );
return null;
}
}
+
+ /**
+ * A helper function that checks whether the request path is defined in shiro
+ * config, specifically under the urls section with anon authentication. e.g.
+ *
+ * urls./knoxtoken/api/v1/jwks.json
+ * anon
+ *
+ *
+ * @param request
+ * @return true if request has anon auth.
+ * @throws URISyntaxException
+ */
+ private static boolean isRequestPathInShiroConfig(
+ final HttpServletRequest request) throws URISyntaxException {
+ boolean isPathInConfig = false;
+ final String requestContextPath = StringUtils.startsWith(
+ request.getPathInfo(), "/") ?
+ request.getPathInfo() :
+ "/" + request.getPathInfo();
+ final Template requestUrlTemplate = Parser.parseLiteral(requestContextPath);
+ for (final Matcher m : anonUrls) {
+ if (m.match(requestUrlTemplate) != null) {
+ isPathInConfig = true;
+ }
+ }
+ return isPathInConfig;
+ }
}
diff --git a/gateway-release/home/conf/topologies/sandbox.xml b/gateway-release/home/conf/topologies/sandbox.xml
index d6a4dbcef3..646bd8cc2f 100644
--- a/gateway-release/home/conf/topologies/sandbox.xml
+++ b/gateway-release/home/conf/topologies/sandbox.xml
@@ -56,6 +56,14 @@
main.ldapRealm.contextFactory.authenticationMechanism
simple
+
+ urls./knoxtoken/api/v2/jwks.json
+ anon
+
+
+ urls./knoxtoken/api/v1/jwks.json
+ anon
+
urls./**
authcBasic
diff --git a/gateway-test/src/test/java/org/apache/knox/gateway/GatewayShiroAuthTest.java b/gateway-test/src/test/java/org/apache/knox/gateway/GatewayShiroAuthTest.java
index 3a199a53c0..ce13a493c2 100644
--- a/gateway-test/src/test/java/org/apache/knox/gateway/GatewayShiroAuthTest.java
+++ b/gateway-test/src/test/java/org/apache/knox/gateway/GatewayShiroAuthTest.java
@@ -48,12 +48,16 @@ public class GatewayShiroAuthTest {
private static final Logger LOG = LoggerFactory.getLogger(GatewayShiroAuthTest.class);
private static final String SHIRO_URL_PATTERN_VALID = "/**";
private static final String SHIRO_URL_PATTERN_INVALID = "/invalid/**";
+ private static final String SHIRO_URL_PATTERN_JWKS = "/knoxtoken/api/v1/jwks.json";
private static final String USERNAME = "guest";
private static final String PASSWORD = "guest-password";
private static final String TOPOLOGY_VALID = "shiro-test-cluster";
private static final String TOPOLOGY_IN_VALID_URL = "shiro-test-cluster-invalid";
private static final String TOPOLOGY_BLOCK_UNSAFE_CHARS = "shiro-test-cluster-unsafe";
+ private static final String TOPOLOGY_JWKS_URL = "shiro-test-jwks";
+ private static final String TOPOLOGY_INVALID_JWKS_URL = "shiro-test-invalid-jwks";
private static final String SERVICE_RESOURCE_NAME = "/test-service-path/test-service-resource";
+ private static final String SERVICE_TOKEN_STATE_NAME = "/knoxtoken/api/v1/jwks.json";
public static GatewayConfig config;
public static GatewayServer gateway;
public static String gatewayUrl;
@@ -116,6 +120,18 @@ public static void setupGateway() throws Exception {
createTopology(SHIRO_URL_PATTERN_VALID, true ).toStream(stream);
}
+ /* create a topology with valid shiro url */
+ File jwks_descriptor = new File(topoDir, TOPOLOGY_JWKS_URL+".xml");
+ try (OutputStream stream = Files.newOutputStream(jwks_descriptor.toPath())) {
+ createJWKSTestTopology(SHIRO_URL_PATTERN_JWKS).toStream(stream);
+ }
+
+ /* create a topology with valid shiro url */
+ File jwks_invalid_descriptor = new File(topoDir, TOPOLOGY_INVALID_JWKS_URL+".xml");
+ try (OutputStream stream = Files.newOutputStream(jwks_invalid_descriptor.toPath())) {
+ createJWKSTestTopology("/some-random-path/*").toStream(stream);
+ }
+
DefaultGatewayServices srvcs = new DefaultGatewayServices();
Map options = new HashMap<>();
options.put("persist-master", "false");
@@ -215,6 +231,40 @@ private static XMLTag createTopology(final String pattern, final boolean blockUn
}
}
+ private static XMLTag createJWKSTestTopology(final String pattern) {
+ return XMLDoc.newDocument(true)
+ .addRoot("topology")
+ .addTag("gateway")
+ .addTag("provider")
+ .addTag("role").addText("authentication")
+ .addTag("name").addText("ShiroProvider")
+ .addTag("enabled").addText("true")
+ .addTag("param")
+ .addTag("name").addText("main.ldapRealm")
+ .addTag("value").addText("org.apache.knox.gateway.shirorealm.KnoxLdapRealm").gotoParent()
+ .addTag("param")
+ .addTag("name").addText("main.ldapRealm.userDnTemplate")
+ .addTag("value").addText("uid={0},ou=people,dc=hadoop,dc=apache,dc=org").gotoParent()
+ .addTag("param")
+ .addTag("name").addText("main.ldapRealm.contextFactory.url")
+ .addTag("value").addText(driver.getLdapUrl()).gotoParent()
+ .addTag("param")
+ .addTag("name").addText("main.ldapRealm.contextFactory.authenticationMechanism")
+ .addTag("value").addText("simple").gotoParent()
+ .addTag("param")
+ .addTag("name").addText("urls." + pattern)
+ .addTag("value").addText("anon").gotoParent().gotoParent()
+ .addTag("provider")
+ .addTag("role").addText("identity-assertion")
+ .addTag("enabled").addText("true")
+ .addTag("name").addText("Default").gotoParent()
+ .addTag("provider")
+ .gotoRoot()
+ .addTag("service")
+ .addTag("role").addText("KNOXTOKEN")
+ .gotoRoot();
+ }
+
/**
* Make sure semicolons are allowed in the URL out-of-the-box
*/
@@ -266,4 +316,28 @@ private void testShiroAuthSuccess(final String serviceUrl, final String expected
.when().get(serviceUrl);
}
+ /**
+ * Test a case where configured shiro url is invalid and there is auth failure.
+ */
+ @Test
+ public void testJWKSEndpointShiro() {
+ final String serviceUrl = gatewayUrl + '/' + TOPOLOGY_JWKS_URL + SERVICE_TOKEN_STATE_NAME;
+ given()
+ .auth().none()
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .contentType("")
+ .body(containsString("{\"keys\":[{\"kty\":\"RSA\""))
+ .when().get(serviceUrl);
+ }
+
+ /**
+ * Test a case where configured shiro url is invalid and there is auth failure.
+ */
+ @Test
+ public void testUnConfiguredJWKSEndpointShiro() {
+ final String serviceUrl = gatewayUrl + '/' + TOPOLOGY_INVALID_JWKS_URL + SERVICE_TOKEN_STATE_NAME;
+ testShiroAuthFailure(HttpStatus.SC_INTERNAL_SERVER_ERROR, serviceUrl, "Unable to determine authenticated user from Shiro, please check that your Knox Shiro configuration is correct");
+ }
+
}