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 {
             
"(&amp;(|(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

Reply via email to