This is an automated email from the ASF dual-hosted git repository.
amagyar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new ed91811 KNOX-2707 - Virtual Group Mapping Provider (#537)
ed91811 is described below
commit ed91811f811ef15c25bc8d5c8ea75facd4490026
Author: Attila Magyar <[email protected]>
AuthorDate: Tue Mar 1 15:28:40 2022 +0100
KNOX-2707 - Virtual Group Mapping Provider (#537)
---
gateway-provider-identity-assertion-common/pom.xml | 5 +
.../knox/gateway/IdentityAsserterMessages.java | 22 +++
.../filter/CommonIdentityAssertionFilter.java | 78 ++++++++-
.../common/filter/VirtualGroupMapper.java | 95 +++++++++++
.../common/filter/VirtualGroupMapperTest.java | 127 +++++++++++++++
.../filter/CommonIdentityAssertionFilterTest.java | 77 ++++-----
.../filter/HadoopGroupProviderFilterTest.java | 2 +-
.../IdentityAsserterDeploymentContributor.java | 6 +-
.../knox/gateway/plang/AbstractSyntaxTree.java | 100 ++++++++++++
.../java/org/apache/knox/gateway/plang/Arity.java | 30 +++-
.../apache/knox/gateway/plang/ArityException.java | 18 ++-
.../org/apache/knox/gateway/plang/Interpreter.java | 128 +++++++++++++++
.../knox/gateway/plang/InterpreterException.java | 16 +-
.../java/org/apache/knox/gateway/plang/Parser.java | 109 +++++++++++++
.../apache/knox/gateway/plang/SyntaxException.java | 14 +-
.../apache/knox/gateway/plang/TypeException.java | 14 +-
.../gateway/plang/UndefinedSymbolException.java | 14 +-
.../apache/knox/gateway/plang/InterpreterTest.java | 178 +++++++++++++++++++++
.../org/apache/knox/gateway/plang/ParserTest.java | 157 ++++++++++++++++++
19 files changed, 1099 insertions(+), 91 deletions(-)
diff --git a/gateway-provider-identity-assertion-common/pom.xml
b/gateway-provider-identity-assertion-common/pom.xml
index 50cae0a..7d7b3c5 100644
--- a/gateway-provider-identity-assertion-common/pom.xml
+++ b/gateway-provider-identity-assertion-common/pom.xml
@@ -104,6 +104,11 @@
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-api</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
diff --git
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
b/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
index 0aee0da..7f373fd 100644
---
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
+++
b/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
@@ -17,12 +17,34 @@
*/
package org.apache.knox.gateway;
+import java.util.Set;
+
import org.apache.knox.gateway.i18n.messages.Message;
import org.apache.knox.gateway.i18n.messages.MessageLevel;
import org.apache.knox.gateway.i18n.messages.Messages;
+import org.apache.knox.gateway.plang.AbstractSyntaxTree;
+import org.apache.knox.gateway.plang.SyntaxException;
@Messages(logger="org.apache.knox.gateway")
public interface IdentityAsserterMessages {
@Message( level = MessageLevel.ERROR, text = "Required subject/identity not
available. Check authentication/federation provider for proper configuration."
)
void subjectNotAvailable();
+
+ @Message( level = MessageLevel.WARN, text = "Invalid mapping parameter name:
Missing required group name.")
+ void missingVirtualGroupName();
+
+ @Message( level = MessageLevel.WARN, text = "Invalid mapping {0}={1}, Parse
error: {2}")
+ void parseError(String key, String script, SyntaxException e);
+
+ @Message( level = MessageLevel.WARN, text = "Invalid result: {2}. Expected
boolean when evaluating group {0} mapping value {1}.")
+ void invalidResult(String virtualGroupName, AbstractSyntaxTree ast, Object
result);
+
+ @Message( level = MessageLevel.DEBUG, text = "Adding user {0} to group {1}
based on predicate {2}")
+ void addingUserToVirtualGroup(String username, String virtualGroupName,
AbstractSyntaxTree ast);
+
+ @Message( level = MessageLevel.DEBUG, text = "Checking whether user {0}
(with group(s) {1}) should be added to group {2} based on predicate {3}")
+ void checkingVirtualGroup(String userName, Set<String> userGroups, String
virtualGroupName, AbstractSyntaxTree ast);
+
+ @Message( level = MessageLevel.DEBUG, text = "User {0} (with group(s) {1})
added to group(s) {2}")
+ void virtualGroups(String userName, Set<String> userGroups, Set<String>
virtualGroups);
}
diff --git
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/CommonIdentityAssertionFilter.java
b/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/CommonIdentityAssertionFilter.java
index c3cea85..adf349d 100644
---
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/CommonIdentityAssertionFilter.java
+++
b/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/CommonIdentityAssertionFilter.java
@@ -17,9 +17,19 @@
*/
package org.apache.knox.gateway.identityasserter.common.filter;
+import java.io.IOException;
+import java.security.AccessController;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
import javax.security.auth.Subject;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -27,20 +37,25 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.knox.gateway.IdentityAsserterMessages;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.plang.AbstractSyntaxTree;
+import org.apache.knox.gateway.plang.Parser;
+import org.apache.knox.gateway.plang.SyntaxException;
+import org.apache.knox.gateway.security.GroupPrincipal;
import org.apache.knox.gateway.security.principal.PrincipalMappingException;
import org.apache.knox.gateway.security.principal.SimplePrincipalMapper;
-import java.io.IOException;
-import java.security.AccessController;
-
public class CommonIdentityAssertionFilter extends
AbstractIdentityAssertionFilter {
+ public static final String VIRTUAL_GROUP_MAPPING_PREFIX = "group.mapping.";
private IdentityAsserterMessages LOG =
MessagesFactory.get(IdentityAsserterMessages.class);
public static final String GROUP_PRINCIPAL_MAPPING =
"group.principal.mapping";
public static final String PRINCIPAL_MAPPING = "principal.mapping";
private SimplePrincipalMapper mapper = new SimplePrincipalMapper();
+ private final Parser parser = new Parser();
+ private VirtualGroupMapper virtualGroupMapper;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
@@ -59,6 +74,55 @@ public class CommonIdentityAssertionFilter extends
AbstractIdentityAssertionFilt
throw new ServletException("Unable to load principal mapping table.",
e);
}
}
+ virtualGroupMapper = new
VirtualGroupMapper(loadVirtualGroups(filterConfig));
+ }
+
+ private Map<String, AbstractSyntaxTree> loadVirtualGroups(FilterConfig
filterConfig) {
+ Map<String, AbstractSyntaxTree> predicateToGroupMapping = new HashMap<>();
+ loadVirtualGroupConfig(filterConfig, predicateToGroupMapping);
+ if (predicateToGroupMapping.isEmpty() && filterConfig.getServletContext()
!= null) {
+ loadVirtualGroupConfig(filterConfig.getServletContext(),
predicateToGroupMapping);
+ }
+ if
(predicateToGroupMapping.keySet().stream().anyMatch(StringUtils::isBlank)) {
+ LOG.missingVirtualGroupName();
+ }
+ return predicateToGroupMapping;
+ }
+
+ private void loadVirtualGroupConfig(FilterConfig config, Map<String,
AbstractSyntaxTree> result) {
+ for (String paramName :
virtualGroupParameterNames(config.getInitParameterNames())) {
+ try {
+ AbstractSyntaxTree ast =
parser.parse(config.getInitParameter(paramName));
+
result.put(paramName.substring(VIRTUAL_GROUP_MAPPING_PREFIX.length()).trim(),
ast);
+ } catch (SyntaxException e) {
+ LOG.parseError(paramName, config.getInitParameter(paramName), e);
+ }
+ }
+ }
+
+ private void loadVirtualGroupConfig(ServletContext context, Map<String,
AbstractSyntaxTree> result) {
+ for (String paramName :
virtualGroupParameterNames(context.getInitParameterNames())) {
+ try {
+ AbstractSyntaxTree ast =
parser.parse(context.getInitParameter(paramName));
+
result.put(paramName.substring(VIRTUAL_GROUP_MAPPING_PREFIX.length()).trim(),
ast);
+ } catch (SyntaxException e) {
+ LOG.parseError(paramName, context.getInitParameter(paramName), e);
+ }
+ }
+ }
+
+ private static List<String> virtualGroupParameterNames(Enumeration<String>
initParameterNames) {
+ List<String> result = new ArrayList<>();
+ if (initParameterNames == null) {
+ return result;
+ }
+ while (initParameterNames.hasMoreElements()) {
+ String name = initParameterNames.nextElement();
+ if (name.startsWith(VIRTUAL_GROUP_MAPPING_PREFIX)) {
+ result.add(name);
+ }
+ }
+ return result;
}
@Override
@@ -86,7 +150,9 @@ public class CommonIdentityAssertionFilter extends
AbstractIdentityAssertionFilt
mappedPrincipalName = mapUserPrincipal(mappedPrincipalName);
String[] mappedGroups = mapGroupPrincipalsBase(mappedPrincipalName,
subject);
String[] groups = mapGroupPrincipals(mappedPrincipalName, subject);
+ String[] virtualGroups = virtualGroupMapper.mapGroups(mappedPrincipalName,
groups(subject), request).toArray(new String[0]);
groups = combineGroupMappings(mappedGroups, groups);
+ groups = combineGroupMappings(virtualGroups, groups);
HttpServletRequestWrapper wrapper = wrapHttpServletRequest(
request, mappedPrincipalName);
@@ -120,6 +186,12 @@ public class CommonIdentityAssertionFilter extends
AbstractIdentityAssertionFilt
return mapper.mapUserPrincipal(principalName);
}
+ private Set<String> groups(Subject subject) {
+ return subject.getPrincipals(GroupPrincipal.class).stream()
+ .map(GroupPrincipal::getName)
+ .collect(Collectors.toSet());
+ }
+
@Override
public String[] mapGroupPrincipals(String mappedPrincipalName, Subject
subject) {
// NOP
diff --git
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/VirtualGroupMapper.java
b/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/VirtualGroupMapper.java
new file mode 100644
index 0000000..7a99119
--- /dev/null
+++
b/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/VirtualGroupMapper.java
@@ -0,0 +1,95 @@
+/*
+ * 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.identityasserter.common.filter;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+import org.apache.knox.gateway.IdentityAsserterMessages;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.plang.Arity;
+import org.apache.knox.gateway.plang.AbstractSyntaxTree;
+import org.apache.knox.gateway.plang.Interpreter;
+
+public class VirtualGroupMapper {
+ private final IdentityAsserterMessages LOG =
MessagesFactory.get(IdentityAsserterMessages.class);
+ private final Map<String, AbstractSyntaxTree> virtualGroupToPredicateMap;
+
+ public VirtualGroupMapper(Map<String, AbstractSyntaxTree>
virtualGroupToPredicateMap) {
+ this.virtualGroupToPredicateMap = virtualGroupToPredicateMap;
+ }
+
+ /**
+ * @return all virtual groups where the corresponding predicate matches
+ */
+ public Set<String> mapGroups(String username, Set<String> groups,
ServletRequest request) {
+ Set<String> virtualGroups = new HashSet<>();
+ for (Map.Entry<String, AbstractSyntaxTree> each :
virtualGroupToPredicateMap.entrySet()) {
+ String virtualGroupName = each.getKey();
+ AbstractSyntaxTree predicate = each.getValue();
+ if (evalPredicate(virtualGroupName, username, groups, predicate,
request)) {
+ virtualGroups.add(virtualGroupName);
+ LOG.addingUserToVirtualGroup(username, virtualGroupName,
predicate);
+ }
+ }
+ LOG.virtualGroups(username, groups, virtualGroups);
+ return virtualGroups;
+ }
+
+ /**
+ * @return true if the user should be added to the virtual group based on
the given predicate
+ */
+ private boolean evalPredicate(String virtualGroupName, String userName,
Set<String> ldapGroups, AbstractSyntaxTree predicate, ServletRequest request) {
+ Interpreter interpreter = new Interpreter();
+ interpreter.addConstant("username", userName);
+ interpreter.addConstant("groups", new ArrayList<>(ldapGroups));
+ addRequestFunctions(request, interpreter);
+ LOG.checkingVirtualGroup(userName, ldapGroups, virtualGroupName,
predicate);
+ Object result = interpreter.eval(predicate);
+ if (!(result instanceof Boolean)) {
+ LOG.invalidResult(virtualGroupName, predicate, result);
+ return false;
+ }
+ return (boolean)result;
+ }
+
+ private void addRequestFunctions(ServletRequest req, Interpreter
interpreter) {
+ if (req instanceof HttpServletRequest) {
+ interpreter.addFunction("request-attribute", Arity.UNARY, params ->
+ ensureNotNull(req.getAttribute((String)params.get(0))));
+ interpreter.addFunction("request-header", Arity.UNARY, params ->
+ ensureNotNull(((HttpServletRequest)
req).getHeader((String)params.get(0))));
+ interpreter.addFunction("session", Arity.UNARY, params ->
+ ensureNotNull(sessionAttribute((HttpServletRequest) req,
(String)params.get(0))));
+ }
+ }
+
+ private String ensureNotNull(Object value) {
+ return value == null ? "" : value.toString();
+ }
+
+ private Object sessionAttribute(HttpServletRequest req, String key) {
+ HttpSession session = req.getSession(false);
+ return session != null ? session.getAttribute(key) : "";
+ }
+}
diff --git
a/gateway-provider-identity-assertion-common/src/test/java/org/apache/knox/gateway/identityasserter/common/filter/VirtualGroupMapperTest.java
b/gateway-provider-identity-assertion-common/src/test/java/org/apache/knox/gateway/identityasserter/common/filter/VirtualGroupMapperTest.java
new file mode 100644
index 0000000..71bc83d
--- /dev/null
+++
b/gateway-provider-identity-assertion-common/src/test/java/org/apache/knox/gateway/identityasserter/common/filter/VirtualGroupMapperTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.identityasserter.common.filter;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.knox.gateway.plang.AbstractSyntaxTree;
+import org.apache.knox.gateway.plang.Parser;
+import org.junit.Test;
+
+@SuppressWarnings("PMD.NonStaticInitializer")
+public class VirtualGroupMapperTest {
+ private Parser parser = new Parser();
+ private VirtualGroupMapper mapper;
+
+ @Test
+ public void testWithEmptyConfig() {
+ mapper = new VirtualGroupMapper(Collections.emptyMap());
+ assertEquals(Collections.emptySet(), virtualGroups("user1",
emptyList()));
+ }
+
+ @Test
+ public void testEverybodyGroup() {
+ mapper = new VirtualGroupMapper(new HashMap<String,
AbstractSyntaxTree>(){{
+ put("everybody", parser.parse("true"));
+ }});
+ assertEquals(setOf("everybody"), virtualGroups("user1", emptyList()));
+ assertEquals(setOf("everybody"), virtualGroups("user2", asList("a",
"b", "c")));
+ }
+
+ @Test
+ public void testNobodyGroup() {
+ mapper = new VirtualGroupMapper(new HashMap<String,
AbstractSyntaxTree>(){{
+ put("nobody", parser.parse("false"));
+ }});
+ assertEquals(0, virtualGroups("user1", emptyList()).size());
+ assertEquals(0, virtualGroups("user2", asList("a", "b", "c")).size());
+ }
+
+ @Test
+ public void testMember() {
+ mapper = new VirtualGroupMapper(new HashMap<String,
AbstractSyntaxTree>(){{
+ put("vg1", parser.parse("(member 'g1')"));
+ put("vg2", parser.parse("(member 'g2')"));
+ put("both", parser.parse("(and (member 'g1') (member 'g2'))"));
+ put("none", parser.parse("(not (or (member 'g1') (member
'g2')))"));
+ }});
+ assertEquals(setOf("vg1"), virtualGroups("user1",
singletonList("g1")));
+ assertEquals(setOf("vg2"), virtualGroups("user2",
singletonList("g2")));
+ assertEquals(setOf("vg1","vg2", "both"), virtualGroups("user3",
asList("g1", "g2")));
+ assertEquals(setOf("none"), virtualGroups("user4",
singletonList("g4")));
+ }
+
+ @Test
+ public void testAtLeastOne() {
+ mapper = new VirtualGroupMapper(new HashMap<String,
AbstractSyntaxTree>(){{
+ put("at-least-one", parser.parse("(!= 0 (size groups))"));
+ }});
+ assertEquals(0, virtualGroups("user1", emptyList()).size());
+ assertEquals(setOf("at-least-one"), virtualGroups("user2",
singletonList("g1")));
+ assertEquals(setOf("at-least-one"), virtualGroups("user3",
asList("g1", "g2")));
+ assertEquals(setOf("at-least-one"), virtualGroups("user4",
asList("g1", "g2", "g3")));
+ }
+
+ @Test
+ public void testEmptyGroup() {
+ mapper = new VirtualGroupMapper(new HashMap<String,
AbstractSyntaxTree>(){{
+ put("empty", parser.parse("(= 0 (size groups))"));
+ }});
+ assertEquals(setOf("empty"), virtualGroups("user1", emptyList()));
+ assertEquals(0, virtualGroups("user2", singletonList("any")).size());
+ }
+
+ @Test
+ public void testMatchUser() {
+ mapper = new VirtualGroupMapper(new HashMap<String,
AbstractSyntaxTree>(){{
+ put("users", parser.parse("(match username 'user_\\d+')"));
+ }});
+ assertEquals(setOf("users"), virtualGroups("user_1", emptyList()));
+ assertEquals(setOf("users"), virtualGroups("user_2", emptyList()));
+ assertEquals(0, virtualGroups("user2", emptyList()).size());
+ }
+
+ @Test
+ public void testMatchGroup() {
+ mapper = new VirtualGroupMapper(new HashMap<String,
AbstractSyntaxTree>(){{
+ put("grp", parser.parse("(match groups 'grp_\\d+')"));
+ }});
+ assertEquals(setOf("grp"), virtualGroups("user1",
singletonList("grp_1")));
+ assertEquals(setOf("grp"), virtualGroups("user2", asList("any",
"grp_2")));
+ assertEquals(0, virtualGroups("user3", singletonList("grp2")).size());
+ assertEquals(0, virtualGroups("user4", emptyList()).size());
+ }
+
+ private Set<String> virtualGroups(String user1, List<String> ldapGroups) {
+ return mapper.mapGroups(user1, new HashSet<>(ldapGroups), null);
+ }
+
+ private static Set<String> setOf(String... strings) {
+ return new HashSet<>(Arrays.asList(strings));
+ }
+}
\ No newline at end of file
diff --git
a/gateway-provider-identity-assertion-common/src/test/java/org/apache/knox/gateway/identityasserter/filter/CommonIdentityAssertionFilterTest.java
b/gateway-provider-identity-assertion-common/src/test/java/org/apache/knox/gateway/identityasserter/filter/CommonIdentityAssertionFilterTest.java
index ffc9975..c659a02 100644
---
a/gateway-provider-identity-assertion-common/src/test/java/org/apache/knox/gateway/identityasserter/filter/CommonIdentityAssertionFilterTest.java
+++
b/gateway-provider-identity-assertion-common/src/test/java/org/apache/knox/gateway/identityasserter/filter/CommonIdentityAssertionFilterTest.java
@@ -17,33 +17,38 @@
*/
package org.apache.knox.gateway.identityasserter.filter;
-import
org.apache.knox.gateway.identityasserter.common.filter.CommonIdentityAssertionFilter;
-import org.apache.knox.gateway.security.GroupPrincipal;
-import org.apache.knox.gateway.security.PrimaryPrincipal;
-import org.easymock.EasyMock;
-import org.junit.Before;
-import org.junit.Test;
+import static
org.apache.knox.gateway.audit.log4j.audit.Log4jAuditService.MDC_AUDIT_CONTEXT_KEY;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import java.io.IOException;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
import javax.security.auth.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 javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
-import java.security.PrivilegedActionException;
-import java.security.PrivilegedExceptionAction;
-import java.util.Locale;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import
org.apache.knox.gateway.identityasserter.common.filter.CommonIdentityAssertionFilter;
+import org.apache.knox.gateway.security.GroupPrincipal;
+import org.apache.knox.gateway.security.PrimaryPrincipal;
+import org.apache.logging.log4j.ThreadContext;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
public class CommonIdentityAssertionFilterTest {
private String username;
private Filter filter;
+ private Set<String> calculatedGroups = new HashSet<>();
@Before
public void setUp() {
@@ -67,15 +72,11 @@ public class CommonIdentityAssertionFilterTest {
@Override
protected String[] combineGroupMappings(String[] mappedGroups, String[]
groups) {
- String[] combined = super.combineGroupMappings(mappedGroups, groups);
- assertEquals("LARRY", username);
- assertTrue("Should be greater than 2", combined.length > 2);
- assertTrue(combined[0], combined[0].equalsIgnoreCase("EVERYONE"));
- assertTrue(combined[1].equalsIgnoreCase("USERS") ||
combined[1].equalsIgnoreCase("ADMIN"));
- assertTrue(combined[2], combined[2].equalsIgnoreCase("USERS") ||
combined[2].equalsIgnoreCase("ADMIN"));
- return combined;
+
calculatedGroups.addAll(Arrays.asList(super.combineGroupMappings(mappedGroups,
groups)));
+ return super.combineGroupMappings(mappedGroups, groups);
}
};
+ ThreadContext.put(MDC_AUDIT_CONTEXT_KEY, "dummy");
}
@Test
@@ -85,6 +86,14 @@ public class CommonIdentityAssertionFilterTest {
andReturn("*=everyone;").once();
EasyMock.expect(config.getInitParameter(CommonIdentityAssertionFilter.PRINCIPAL_MAPPING)).
andReturn("ljm=lmccay;").once();
+ EasyMock.expect(config.getInitParameterNames()).
+ andReturn(Collections.enumeration(Arrays.asList(
+ CommonIdentityAssertionFilter.GROUP_PRINCIPAL_MAPPING,
+ CommonIdentityAssertionFilter.PRINCIPAL_MAPPING,
+ CommonIdentityAssertionFilter.VIRTUAL_GROUP_MAPPING_PREFIX
+ "test-virtual-group")))
+ .anyTimes();
+
EasyMock.expect(config.getInitParameter(CommonIdentityAssertionFilter.VIRTUAL_GROUP_MAPPING_PREFIX
+ "test-virtual-group")).
+ andReturn("(and (username 'lmccay') (and (member 'users') (member
'admin')))").anyTimes();
EasyMock.replay( config );
final HttpServletRequest request = EasyMock.createNiceMock(
HttpServletRequest.class );
@@ -93,28 +102,20 @@ public class CommonIdentityAssertionFilterTest {
final HttpServletResponse response = EasyMock.createNiceMock(
HttpServletResponse.class );
EasyMock.replay( response );
- final FilterChain chain = new FilterChain() {
- @Override
- public void doFilter(ServletRequest request, ServletResponse response)
- throws IOException, ServletException {
- }
- };
+ final FilterChain chain = (req, resp) -> {};
Subject subject = new Subject();
- subject.getPrincipals().add(new PrimaryPrincipal("larry"));
+ subject.getPrincipals().add(new PrimaryPrincipal("ljm"));
subject.getPrincipals().add(new GroupPrincipal("users"));
subject.getPrincipals().add(new GroupPrincipal("admin"));
try {
Subject.doAs(
subject,
- new PrivilegedExceptionAction<Object>() {
- @Override
- public Object run() throws Exception {
- filter.init(config);
- filter.doFilter(request, response, chain);
- return null;
- }
- });
+ (PrivilegedExceptionAction<Object>) () -> {
+ filter.init(config);
+ filter.doFilter(request, response, chain);
+ return null;
+ });
}
catch (PrivilegedActionException e) {
Throwable t = e.getCause();
@@ -128,5 +129,9 @@ public class CommonIdentityAssertionFilterTest {
throw new ServletException(t);
}
}
+
+ assertEquals("LMCCAY", username);
+ assertTrue("Should be greater than 2", calculatedGroups.size() > 2);
+ assertTrue(calculatedGroups.containsAll(Arrays.asList("everyone", "USERS",
"ADMIN", "test-virtual-group")));
}
}
diff --git
a/gateway-provider-identity-assertion-hadoop-groups/src/test/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderFilterTest.java
b/gateway-provider-identity-assertion-hadoop-groups/src/test/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderFilterTest.java
index ee59411..18bcb0c 100644
---
a/gateway-provider-identity-assertion-hadoop-groups/src/test/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderFilterTest.java
+++
b/gateway-provider-identity-assertion-hadoop-groups/src/test/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderFilterTest.java
@@ -175,7 +175,7 @@ public class HadoopGroupProviderFilterTest {
"(&(|(objectclass=person)(objectclass=applicationProcess))(cn={0}))")
.anyTimes();
EasyMock.expect(config.getInitParameterNames())
- .andReturn(Collections.enumeration((keysList))).anyTimes();
+ .andStubAnswer(() -> Collections.enumeration((keysList)));
EasyMock.replay( config );
EasyMock.replay( context );
diff --git
a/gateway-provider-identity-assertion-pseudo/src/main/java/org/apache/knox/gateway/identityasserter/filter/IdentityAsserterDeploymentContributor.java
b/gateway-provider-identity-assertion-pseudo/src/main/java/org/apache/knox/gateway/identityasserter/filter/IdentityAsserterDeploymentContributor.java
index f6356df..05ea60d 100644
---
a/gateway-provider-identity-assertion-pseudo/src/main/java/org/apache/knox/gateway/identityasserter/filter/IdentityAsserterDeploymentContributor.java
+++
b/gateway-provider-identity-assertion-pseudo/src/main/java/org/apache/knox/gateway/identityasserter/filter/IdentityAsserterDeploymentContributor.java
@@ -17,6 +17,8 @@
*/
package org.apache.knox.gateway.identityasserter.filter;
+import static
org.apache.knox.gateway.identityasserter.common.filter.CommonIdentityAssertionFilter.VIRTUAL_GROUP_MAPPING_PREFIX;
+
import org.apache.knox.gateway.deploy.DeploymentContext;
import
org.apache.knox.gateway.identityasserter.common.filter.AbstractIdentityAsserterDeploymentContributor;
import org.apache.knox.gateway.topology.Provider;
@@ -37,9 +39,11 @@ public class IdentityAsserterDeploymentContributor extends
AbstractIdentityAsser
super.contributeProvider(context, provider);
String mappings = provider.getParams().get(PRINCIPAL_MAPPING_PARAM_NAME);
String groupMappings =
provider.getParams().get(GROUP_PRINCIPAL_MAPPING_PARAM_NAME);
-
context.getWebAppDescriptor().createContextParam().paramName(PRINCIPAL_MAPPING_PARAM_NAME).paramValue(mappings);
context.getWebAppDescriptor().createContextParam().paramName(GROUP_PRINCIPAL_MAPPING_PARAM_NAME).paramValue(groupMappings);
+ provider.getParamsList().stream()
+ .filter(each ->
each.getName().startsWith(VIRTUAL_GROUP_MAPPING_PREFIX))
+ .forEach(each ->
context.getWebAppDescriptor().createContextParam().paramName(each.getName()).paramValue(each.getValue()));
}
@Override
diff --git
a/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/AbstractSyntaxTree.java
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/AbstractSyntaxTree.java
new file mode 100644
index 0000000..fd69155
--- /dev/null
+++
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/AbstractSyntaxTree.java
@@ -0,0 +1,100 @@
+/*
+ * 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.plang;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Abstract Syntax Tree of the code.
+ *
+ * For example from the following code:
+ * (or true (and false true))
+ *
+ * The parser generates the following AST:
+ * [or, true, [and, false, true]]
+ */
+public class AbstractSyntaxTree {
+ private final List<AbstractSyntaxTree> children = new ArrayList<>();
+ private final String token;
+
+ public AbstractSyntaxTree(String token) {
+ this.token = token;
+ }
+
+ public void addChild(AbstractSyntaxTree child) {
+ children.add(child);
+ }
+
+ @Override
+ public String toString() {
+ return isAtom() ? token : children.toString();
+ }
+
+ public String token() {
+ return token;
+ }
+
+ public boolean isStr() {
+ return token != null && token.startsWith("'") && token.endsWith("'");
+ }
+
+ public boolean isNumber() {
+ boolean result = false;
+ if (token != null) {
+ try {
+ numValue();
+ result = true;
+ } catch (NumberFormatException e) {
+ // NaN
+ }
+ }
+ return result;
+ }
+
+ public Number numValue() {
+ try {
+ return Long.parseLong(token);
+ } catch (NumberFormatException e) {
+ return Double.parseDouble(token);
+ }
+ }
+
+ public String strValue() {
+ if (!isStr()) {
+ throw new InterpreterException("Token: " + token + " is not a
String");
+ }
+ return token.substring(1, token.length() -1);
+ }
+
+ public boolean isAtom() {
+ return !"(".equals(token);
+ }
+
+ public boolean isFunction() {
+ return !children.isEmpty();
+ }
+
+ public String functionName() {
+ return children.get(0).token();
+ }
+
+ public List<AbstractSyntaxTree> functionParameters() {
+ return children.subList(1, children.size());
+ }
+}
diff --git
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/Arity.java
similarity index 51%
copy from
gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
copy to
gateway-util-common/src/main/java/org/apache/knox/gateway/plang/Arity.java
index 0aee0da..fb05407 100644
---
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/Arity.java
@@ -15,14 +15,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.knox.gateway;
+package org.apache.knox.gateway.plang;
-import org.apache.knox.gateway.i18n.messages.Message;
-import org.apache.knox.gateway.i18n.messages.MessageLevel;
-import org.apache.knox.gateway.i18n.messages.Messages;
+import java.util.List;
-@Messages(logger="org.apache.knox.gateway")
-public interface IdentityAsserterMessages {
- @Message( level = MessageLevel.ERROR, text = "Required subject/identity not
available. Check authentication/federation provider for proper configuration."
)
- void subjectNotAvailable();
+public interface Arity {
+ void check(String function, List<?> params);
+ Arity UNARY = Arity.of(1);
+ Arity BINARY = Arity.of(2);
+
+ static Arity of(int count) {
+ return (methodName, params) -> {
+ if (params.size() != count) {
+ throw new ArityException(methodName, count, params.size());
+ }
+ };
+ }
+ static Arity min(int count) {
+ return (methodName, params) -> {
+ if (params.size() < count) {
+ throw new ArityException("wrong number of arguments in call to
'" + methodName
+ + "'. Expected at least " + count + " got " +
params.size() + ".");
+ }
+ };
+ }
}
diff --git
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/ArityException.java
similarity index 62%
copy from
gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
copy to
gateway-util-common/src/main/java/org/apache/knox/gateway/plang/ArityException.java
index 0aee0da..7fbac47 100644
---
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
+++
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/ArityException.java
@@ -15,14 +15,16 @@
* 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;
+package org.apache.knox.gateway.plang;
-@Messages(logger="org.apache.knox.gateway")
-public interface IdentityAsserterMessages {
- @Message( level = MessageLevel.ERROR, text = "Required subject/identity not
available. Check authentication/federation provider for proper configuration."
)
- void subjectNotAvailable();
+public class ArityException extends InterpreterException {
+ public ArityException(String funcName, int formalCount, int actualCount) {
+ super("Wrong number of arguments in call to '" + funcName
+ + "'. Expected " + formalCount + " got " + actualCount + ".");
+ }
+
+ public ArityException(String message) {
+ super(message);
+ }
}
diff --git
a/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/Interpreter.java
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/Interpreter.java
new file mode 100644
index 0000000..5037278
--- /dev/null
+++
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/Interpreter.java
@@ -0,0 +1,128 @@
+/*
+ * 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.plang;
+
+import static java.util.stream.Collectors.toList;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public class Interpreter {
+ private static final Logger LOG = LogManager.getLogger(Interpreter.class);
+ private final Map<String, SpecialForm> specialForms = new HashMap<>();
+ private final Map<String, Func> functions = new HashMap<>();
+ private final Map<String, Object> constants = new HashMap<>();
+
+ public interface Func {
+ Object call(List<Object> parameters);
+ }
+
+ private interface SpecialForm {
+ Object call(List<AbstractSyntaxTree> parameters);
+ }
+
+ public Interpreter() {
+ specialForms.put("or", args -> {
+ Arity.min(1).check("or", args);
+ return args.stream().anyMatch(each -> (boolean)eval(each));
+ });
+ specialForms.put("and", args -> {
+ Arity.min(1).check("and", args);
+ return args.stream().allMatch(each -> (boolean)eval(each));
+ });
+ addFunction("not", Arity.UNARY, args -> !(boolean)args.get(0));
+ addFunction("=", Arity.BINARY, args -> equalTo(args.get(0),
args.get(1)));
+ addFunction("!=", Arity.BINARY, args -> !equalTo(args.get(0),
args.get(1)));
+ addFunction("match", Arity.BINARY, args ->
+ args.get(0) instanceof String
+ ? Pattern.matches((String)args.get(1), (String)args.get(0))
+ : ((List<String>)(args.get(0))).stream().anyMatch(each ->
Pattern.matches((String)args.get(1), each))
+ );
+ addFunction("size", Arity.UNARY, args -> ((Collection<?>)
args.get(0)).size());
+ addFunction("empty", Arity.UNARY, args -> ((Collection<?>)
args.get(0)).isEmpty());
+ addFunction("username", Arity.UNARY, args ->
constants.get("username").equals(args.get(0)));
+ addFunction("member", Arity.UNARY, args ->
((List<String>)constants.get("groups")).contains((String)args.get(0)));
+ addFunction("lowercase", Arity.UNARY, args ->
((String)args.get(0)).toLowerCase(Locale.getDefault()));
+ addFunction("uppercase", Arity.UNARY, args ->
((String)args.get(0)).toUpperCase(Locale.getDefault()));
+ addFunction("print", Arity.min(1), args -> { // for debugging
+ args.forEach(arg -> LOG.info(arg == null ? "null" :
arg.toString()));
+ return false;
+ });
+ constants.put("true", true);
+ constants.put("false", false);
+ }
+
+ private static boolean equalTo(Object a, Object b) {
+ if (a instanceof Number && b instanceof Number) {
+ return Double.compare(((Number)a).doubleValue(),
((Number)b).doubleValue()) == 0;
+ } else {
+ return a.equals(b);
+ }
+ }
+
+ public void addConstant(String name, Object value) {
+ constants.put(name, value);
+ }
+
+ public void addFunction(String name, Arity arity, Func func) {
+ functions.put(name, parameters -> {
+ arity.check(name, parameters);
+ return func.call(parameters);
+ });
+ }
+
+ public Object eval(AbstractSyntaxTree ast) {
+ try {
+ if (ast == null) {
+ return null;
+ } else if (ast.isAtom()) {
+ return ast.isStr() ? ast.strValue() : ast.isNumber() ?
ast.numValue() : lookupConstant(ast);
+ } else if (ast.isFunction()) {
+ SpecialForm specialForm = specialForms.get(ast.functionName());
+ if (specialForm != null) {
+ return specialForm.call(ast.functionParameters());
+ } else {
+ Func func = functions.get(ast.functionName());
+ if (func == null) {
+ throw new UndefinedSymbolException(ast.functionName(),
"function");
+ }
+ return
func.call(ast.functionParameters().stream().map(this::eval).collect(toList()));
+ }
+ } else {
+ throw new InterpreterException("Unknown token: " +
ast.token());
+ }
+ } catch (ClassCastException e) {
+ throw new TypeException("Type error at: " + ast, e);
+ }
+ }
+
+ private Object lookupConstant(AbstractSyntaxTree ast) {
+ Object var = constants.get(ast.token());
+ if (var == null) {
+ throw new UndefinedSymbolException(ast.token(), "variable");
+ }
+ return var;
+ }
+}
diff --git
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/InterpreterException.java
similarity index 62%
copy from
gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
copy to
gateway-util-common/src/main/java/org/apache/knox/gateway/plang/InterpreterException.java
index 0aee0da..9133599 100644
---
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
+++
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/InterpreterException.java
@@ -15,14 +15,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.knox.gateway;
+package org.apache.knox.gateway.plang;
-import org.apache.knox.gateway.i18n.messages.Message;
-import org.apache.knox.gateway.i18n.messages.MessageLevel;
-import org.apache.knox.gateway.i18n.messages.Messages;
+public class InterpreterException extends RuntimeException {
+ public InterpreterException(String message) {
+ super(message);
+ }
-@Messages(logger="org.apache.knox.gateway")
-public interface IdentityAsserterMessages {
- @Message( level = MessageLevel.ERROR, text = "Required subject/identity not
available. Check authentication/federation provider for proper configuration."
)
- void subjectNotAvailable();
+ public InterpreterException(String message, Throwable cause) {
+ super(message, cause);
+ }
}
diff --git
a/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/Parser.java
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/Parser.java
new file mode 100644
index 0000000..f185c23
--- /dev/null
+++
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/Parser.java
@@ -0,0 +1,109 @@
+/*
+ * 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.plang;
+
+import java.io.IOException;
+import java.io.PushbackReader;
+import java.io.StringReader;
+
+public class Parser {
+
+ public AbstractSyntaxTree parse(String str) {
+ if (str == null || str.trim().equals("")) {
+ return null;
+ }
+ try (PushbackReader reader = new PushbackReader(new
StringReader(str))) {
+ AbstractSyntaxTree ast = parse(reader);
+ String rest = peek(reader);
+ if (rest != null) {
+ throw new SyntaxException("Unexpected closing " + rest);
+ }
+ return ast;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private AbstractSyntaxTree parse(PushbackReader reader) throws IOException
{
+ String token = nextToken(reader);
+ if ("(".equals(token)) {
+ AbstractSyntaxTree children = new AbstractSyntaxTree(token);
+ while (!")".equals(peek(reader))) {
+ children.addChild(parse(reader));
+ }
+ nextToken(reader); // skip )
+ return children;
+ } else if (")".equals(token)) {
+ throw new SyntaxException("Unexpected closing )");
+ } else if ("".equals(token)) {
+ throw new SyntaxException("Missing closing )");
+ } else {
+ return new AbstractSyntaxTree(token);
+ }
+ }
+
+ private String nextToken(PushbackReader reader) throws IOException {
+ String chr = peek(reader);
+ if ("'".equals(chr)) {
+ return parseString(reader);
+ }
+ if ("(".equals(chr) || ")".equals(chr)) {
+ return String.valueOf((char) reader.read());
+ }
+ return parseAtom(reader);
+ }
+
+ private String parseAtom(PushbackReader reader) throws IOException {
+ StringBuilder buffer = new StringBuilder();
+ int chr = reader.read();
+ while (chr != -1 && !Character.isWhitespace(chr) && ')' != chr) {
+ buffer.append((char)chr);
+ chr = reader.read();
+ }
+ if (chr == ')') {
+ reader.unread(')');
+ }
+ return buffer.toString();
+ }
+
+ private String parseString(PushbackReader reader) throws IOException {
+ StringBuilder str = new StringBuilder();
+ str.append((char)reader.read());
+ int chr = reader.read();
+ while (chr != -1 && '\'' != chr) {
+ str.append((char)chr);
+ chr = reader.read();
+ }
+ if (chr == -1) {
+ throw new SyntaxException("Unterminated string");
+ }
+ return str.append("'").toString();
+ }
+
+ private String peek(PushbackReader reader) throws IOException {
+ int chr = reader.read();
+ while (chr != -1 && Character.isWhitespace(chr)) {
+ chr = reader.read();
+ }
+ if (chr == -1) {
+ return null;
+ }
+ reader.unread(chr);
+ return String.valueOf((char) chr);
+ }
+}
diff --git
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/SyntaxException.java
similarity index 62%
copy from
gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
copy to
gateway-util-common/src/main/java/org/apache/knox/gateway/plang/SyntaxException.java
index 0aee0da..0554df0 100644
---
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
+++
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/SyntaxException.java
@@ -15,14 +15,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.knox.gateway;
+package org.apache.knox.gateway.plang;
-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 IdentityAsserterMessages {
- @Message( level = MessageLevel.ERROR, text = "Required subject/identity not
available. Check authentication/federation provider for proper configuration."
)
- void subjectNotAvailable();
+public class SyntaxException extends InterpreterException {
+ public SyntaxException(String message) {
+ super(message);
+ }
}
diff --git
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/TypeException.java
similarity index 62%
copy from
gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
copy to
gateway-util-common/src/main/java/org/apache/knox/gateway/plang/TypeException.java
index 0aee0da..1a7d85b 100644
---
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
+++
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/TypeException.java
@@ -15,14 +15,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.knox.gateway;
+package org.apache.knox.gateway.plang;
-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 IdentityAsserterMessages {
- @Message( level = MessageLevel.ERROR, text = "Required subject/identity not
available. Check authentication/federation provider for proper configuration."
)
- void subjectNotAvailable();
+public class TypeException extends InterpreterException {
+ public TypeException(String message, Throwable exception) {
+ super(message, exception);
+ }
}
diff --git
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/UndefinedSymbolException.java
similarity index 62%
copy from
gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
copy to
gateway-util-common/src/main/java/org/apache/knox/gateway/plang/UndefinedSymbolException.java
index 0aee0da..f666997 100644
---
a/gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/IdentityAsserterMessages.java
+++
b/gateway-util-common/src/main/java/org/apache/knox/gateway/plang/UndefinedSymbolException.java
@@ -15,14 +15,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.knox.gateway;
+package org.apache.knox.gateway.plang;
-import org.apache.knox.gateway.i18n.messages.Message;
-import org.apache.knox.gateway.i18n.messages.MessageLevel;
-import org.apache.knox.gateway.i18n.messages.Messages;
+import java.util.Locale;
-@Messages(logger="org.apache.knox.gateway")
-public interface IdentityAsserterMessages {
- @Message( level = MessageLevel.ERROR, text = "Required subject/identity not
available. Check authentication/federation provider for proper configuration."
)
- void subjectNotAvailable();
+public class UndefinedSymbolException extends InterpreterException {
+ public UndefinedSymbolException(String name, String type) {
+ super(String.format(Locale.getDefault(), "Undefined %s: %s", type,
name));
+ }
}
diff --git
a/gateway-util-common/src/test/java/org/apache/knox/gateway/plang/InterpreterTest.java
b/gateway-util-common/src/test/java/org/apache/knox/gateway/plang/InterpreterTest.java
new file mode 100644
index 0000000..e2ed2d3
--- /dev/null
+++
b/gateway-util-common/src/test/java/org/apache/knox/gateway/plang/InterpreterTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.plang;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class InterpreterTest {
+ Interpreter interpreter = new Interpreter();
+ Parser parser = new Parser();
+
+ @Test
+ public void testEmpty() {
+ assertNull(eval(null));
+ assertNull(eval(""));
+ assertNull(eval(" "));
+ }
+
+ @Test
+ public void testBooleans() {
+ assertTrue((boolean)eval("true"));
+ assertFalse((boolean)eval("false"));
+ }
+
+ @Test
+ public void testEq() {
+ assertTrue((boolean)eval("(= true true)"));
+ assertTrue((boolean)eval("(= false false)"));
+ assertFalse((boolean)eval("(= true false)"));
+ assertFalse((boolean)eval("(= false true)"));
+ assertFalse((boolean)eval("(= 'apple' 'orange')"));
+ assertTrue((boolean)eval("(= 'apple' 'apple')"));
+ assertTrue((boolean)eval("(= 0 0)"));
+ assertTrue((boolean)eval("(= -10.33242 -10.33242)"));
+ }
+
+ @Test
+ public void testNotEq() {
+ assertFalse((boolean)eval("(!= true true)"));
+ assertFalse((boolean)eval("(!= false false)"));
+ assertTrue((boolean)eval("(!= true false)"));
+ assertTrue((boolean)eval("(!= false true)"));
+ assertTrue((boolean)eval("(!= 'apple' 'orange')"));
+ assertFalse((boolean)eval("(!= 'apple' 'apple')"));
+ assertFalse((boolean)eval("(!= 0 0)"));
+ assertFalse((boolean)eval("(!= -10.33242 -10.33242)"));
+ }
+
+ @Test
+ public void testCmpDifferentTypes() {
+ assertTrue((boolean)eval("(= 1.0 1)"));
+ assertFalse((boolean)eval("(!= 1.0 1)"));
+ assertTrue((boolean)eval("(!= 1.0 2)"));
+ assertFalse((boolean)eval("(= 1.0 2)"));
+ assertTrue((boolean)eval("(!= '12' 12)"));
+ assertFalse((boolean)eval("(= 12 '12')"));
+ }
+
+ @Test
+ public void testOr() {
+ assertTrue((boolean)eval("(or true true)"));
+ assertTrue((boolean)eval("(or true false)"));
+ assertTrue((boolean)eval("(or false true)"));
+ assertFalse((boolean)eval("(or false false)"));
+ assertTrue((boolean)eval("(or false false false true false)"));
+ assertFalse((boolean)eval("(or false false false false false)"));
+ }
+
+ @Test
+ public void testAnd() {
+ assertTrue((boolean)eval("(and true true)"));
+ assertFalse((boolean)eval("(and false false)"));
+ assertFalse((boolean)eval("(and true false)"));
+ assertFalse((boolean)eval("(and false true)"));
+ assertFalse((boolean)eval("(and true true true false)"));
+ assertTrue((boolean)eval("(and true true true true)"));
+ }
+
+ @Test
+ public void testNot() {
+ assertFalse((boolean)eval("(not true)"));
+ assertTrue((boolean)eval("(not false)"));
+ assertFalse((boolean)eval("(not (not false))"));
+ assertTrue((boolean)eval("(not (not true))"));
+ }
+
+ @Test
+ public void testComplex() {
+ assertTrue((boolean)eval("(and (not false) (or (not (or (not true)
(not false) )) true))"));
+ }
+
+ @Test(expected = TypeException.class)
+ public void testTypeError() {
+ eval("(size 12)");
+ }
+
+ @Test
+ public void testStrings() {
+ assertEquals("", eval("''"));
+ assertEquals(" a b c ", eval("' a b c '"));
+ }
+
+ @Test
+ public void testMatchString() {
+ assertTrue((boolean)eval("(match 'user1' 'user\\d+')"));
+ assertTrue((boolean)eval("(match 'user12' 'user\\d+')"));
+ assertFalse((boolean)eval("(match 'user12d' 'user\\d+')"));
+ assertFalse((boolean)eval("(match 'user' 'user\\d+')"));
+ assertFalse((boolean)eval("(match '12' 'user\\d+')"));
+ assertTrue((boolean)eval("(match 'hive' 'hive|joe')"));
+ assertTrue((boolean)eval("(match 'joe' 'hive|joe')"));
+ assertFalse((boolean)eval("(match 'tom' 'hive|joe')"));
+ assertFalse((boolean)eval("(match 'hive1' 'hive|joe')"));
+ assertFalse((boolean)eval("(match '0joe' 'hive|joe')"));
+ }
+
+ @Test
+ public void testMatchList() {
+ interpreter.addConstant("groups", singletonList("grp1"));
+ assertTrue((boolean)eval("(match groups 'grp\\d+')"));
+ interpreter.addConstant("groups", singletonList("grp12"));
+ assertTrue((boolean)eval("(match groups 'grp\\d+')"));
+ interpreter.addConstant("groups", singletonList("grp12d"));
+ assertFalse((boolean)eval("(match groups 'grp\\d+')"));
+ interpreter.addConstant("groups", singletonList("grp"));
+ assertFalse((boolean)eval("(match groups 'grp\\d+')"));
+ interpreter.addConstant("groups", singletonList("12"));
+ assertFalse((boolean)eval("(match groups 'grp\\d+')"));
+ interpreter.addConstant("groups", asList("12", "grp12"));
+ assertTrue((boolean)eval("(match groups 'grp\\d+')"));
+ }
+
+ @Test
+ public void testFuncEmpty() {
+ interpreter.addConstant("groups", emptyList());
+ assertTrue((boolean)eval("(empty groups)"));
+ interpreter.addConstant("groups", singletonList("grp1"));
+ assertFalse((boolean)eval("(empty groups)"));
+ }
+
+ @Test
+ public void testLowerUpper() {
+ assertEquals("apple", eval("(lowercase 'APPLE')"));
+ assertEquals("APPLE", eval("(uppercase 'apple')"));
+ }
+
+ @Test
+ public void testShortCircuitConditionals() {
+ assertTrue((boolean)eval("(or true (invalid-expression 1 2 3))"));
+ assertFalse((boolean)eval("(and false (invalid-expression 1 2 3))"));
+ }
+
+ private Object eval(String script) {
+ return interpreter.eval(parser.parse(script));
+ }
+}
\ No newline at end of file
diff --git
a/gateway-util-common/src/test/java/org/apache/knox/gateway/plang/ParserTest.java
b/gateway-util-common/src/test/java/org/apache/knox/gateway/plang/ParserTest.java
new file mode 100644
index 0000000..85e8db3
--- /dev/null
+++
b/gateway-util-common/src/test/java/org/apache/knox/gateway/plang/ParserTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.plang;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Locale;
+
+public class ParserTest {
+ @Test
+ public void testEmpty() {
+ assertEquals(null, parse(""));
+ assertEquals(null, parse(" "));
+ assertEquals(null, parse(null));
+ }
+
+ @Test
+ public void testUnexpectedClosingParen() {
+ parse(")", "unexpected closing )");
+ parse("())", "unexpected closing )");
+ parse("() )", "unexpected closing )");
+ parse("() ) ", "unexpected closing )");
+ parse("(a (b) ))", "unexpected closing )");
+ parse("( a ( b ( c (d (e (f ( g ) ) )) ) ) ))", "unexpected
closing )");
+ }
+
+ @Test
+ public void testMissingClosingParen() {
+ parse("(", "missing closing )");
+ parse(" (", "missing closing )");
+ parse("( ", "missing closing )");
+ parse(" (a", "missing closing )");
+ parse(" (a ()", "missing closing )");
+ parse(" (a (b ( ) ) ", "missing closing )");
+ parse("( a ( b ( c (d (e (f ( g ) ) )) ) ", "missing closing
)");
+ }
+
+ @Test
+ public void testValidSingle() {
+ assertEquals("[]", parse("()").toString());
+ assertEquals("[]", parse("( )").toString());
+ assertEquals("[]", parse(" ( ) ").toString());
+ assertEquals("[a]", parse("(a)").toString());
+ assertEquals("[a]", parse("( a)").toString());
+ assertEquals("[a]", parse("(a )").toString());
+ assertEquals("[a]", parse("( a )").toString());
+ assertEquals("[a]", parse(" ( a ) ").toString());
+ }
+
+ @Test
+ public void testValidNested1() {
+ assertEquals("[a, [b]]", parse("(a (b))").toString());
+ assertEquals("[a, [b]]", parse(" (a (b))").toString());
+ assertEquals("[a, [b]]", parse(" ( a ( b))").toString());
+ assertEquals("[a, [b]]", parse(" ( a ( b )) ").toString());
+ assertEquals("[a, [b]]", parse(" ( a ( b ) ) ").toString());
+ assertEquals("[a, [b]]", parse("(a (b) )").toString());
+ }
+
+ @Test
+ public void testValidNested2() {
+ assertEquals("[a, [b, c, [d]], e]", parse("(a (b c (d))
e)").toString());
+ assertEquals("[a, [b, c, [d]], e]", parse("(a ( b c ( d))
e)").toString());
+ assertEquals("[a, [b, c, [d]], e]", parse("(a (b c (d ) )
e)").toString());
+ assertEquals("[a, [b, c, [d]], e]", parse(" (a ( b c (d )) e
)").toString());
+ assertEquals("[a, [b, c, [d]], e]", parse(" (a ( b c ( d ) ) e )
").toString());
+ }
+
+ @Test
+ public void testValidNested3() {
+ assertEquals("[ab, [cd], ef]", parse("(ab (cd) ef)").toString());
+ assertEquals("[ab, [cd], ef]", parse("(ab ( cd) ef)").toString());
+ assertEquals("[ab, [cd], ef]", parse("(ab ( cd ) ef)").toString());
+ assertEquals("[ab, [cd], ef]", parse(" ( ab ( cd ) ef ) ").toString());
+ }
+
+ @Test
+ public void testValidNested4() {
+ assertEquals("[a, [b, [c, [d, [e, [f, [g]]]]]]]", parse("(a (b (c (d
(e (f (g)))))))").toString());
+ assertEquals("[a, [b, [c, [d, [e, [f, [g]]]]]]]", parse("( a ( b (c
(d (e (f ( g)) )) )))").toString());
+ assertEquals("[a, [b, [c, [d, [e, [f, [g]]]]]]]", parse("( a ( b (c
(d (e (f ( g) ) )) ) ) )").toString());
+ assertEquals("[a, [b, [c, [d, [e, [f, [g]]]]]]]", parse("( a ( b ( c
(d (e (f ( g ) ) )) ) ) )").toString());
+ }
+
+ @Test
+ public void testValidStrings() {
+ assertEquals("''", parse("''").toString());
+ assertEquals("' '", parse("' '").toString());
+ assertEquals("'abc'", parse("'abc'").toString());
+ assertEquals("'a (bc'", parse("'a (bc'").toString());
+ assertEquals("'ab)c'", parse("'ab)c'").toString());
+ assertEquals("'ab) c'", parse("'ab) c'").toString());
+ assertEquals("'abc'", parse(" 'abc' ").toString());
+ assertEquals("'ab c'", parse(" 'ab c' ").toString());
+ assertEquals("'a b c'", parse(" 'a b c' ").toString());
+ assertEquals("['abc']", parse("('abc')").toString());
+ assertEquals("['abc']", parse("( 'abc')").toString());
+ assertEquals("['abc']", parse("( 'abc' )").toString());
+ assertEquals("[' a b c ']", parse(" ( ' a b c ' ) ").toString());
+ assertEquals("[['a', [''], ['b c', 'd']]]", parse(" ( ('a' ('') ('b c'
'd') )) ").toString());
+ }
+
+ @Test
+ public void testInvalidStrings() {
+ parse("'", "unterminated string");
+ parse(" ' ", "unterminated string");
+ parse(" 'a ", "unterminated string");
+ parse(" 'a b ", "unterminated string");
+ parse("( 'a b ", "unterminated string");
+ parse(" 'a b ) ", "unterminated string");
+ }
+
+ @Test
+ public void testNumbers() {
+ assertEquals("0", parse("0").toString());
+ assertEquals("1", parse("1").toString());
+ assertEquals("-1", parse("-1").toString());
+ assertEquals("+1", parse("+1").toString());
+ assertEquals("1.2342424", parse("1.2342424").toString());
+ }
+
+ private static AbstractSyntaxTree parse(String script) {
+ Parser parser = new Parser();
+ return parser.parse(script);
+ }
+
+ private static void parse(String script, String message) {
+ Parser parser = new Parser();
+ try {
+ parser.parse(script);
+ fail("Expected syntax error with message: " + message);
+ } catch (SyntaxException e) {
+ assertTrue(
+ "Expected syntax error: " + message + " but got: " +
e.getMessage(),
+
e.getMessage().toLowerCase(Locale.getDefault()).contains(message.toLowerCase(Locale.getDefault())));
+ }
+ }
+}
\ No newline at end of file