Java EE authorization - JACC revisited part II

This is the second part of a series where we revisit JACC after taking an initial look at it last year. In the first part we somewhat rectified a few of the disadvantages that were initially discovered and looked at various role mapping strategies.

In this second part we'll take an in-depth look at obtaining the container specific role mapper and the container specific way of how a JACC provider is deployed. In the next and final part we'll be bringing it all together and present a fully working JACC provider.

Container specifics

The way in which to obtain the role mapper and what data it exactly provides differs greatly for each container, and is something that containers don't really document either. Also, although the two system properties that need to be specified for the two JACC artifacts are standardized, it's often not at all clear how the jar file containing the JACC provider implementation classes has to be added to the container's class path.

After much research I obtained the details on how to do this for the following servers:

  • GlassFish 4.1
  • WebLogic 12.1.3
  • Geronimo 3.0.1
This list is admittedly limited, but as it appeared the process of finding out these details can be rather time consuming and frankly maddening. Given the amount of time that already went into this research I decided to leave it at these three, but hope to look into additional servers at a later date.

The JACC provider that we'll present in the next part will use a RoleMapper class that at runtime tries to obtain the native mapper from each known server using reflection (so to avoid compile dependencies). Whatever the native role mapper returns is transformed to a group to roles map first (see part I for more details on the various mappings). In the section below the specific reflective code for each server is given first. The full RoleMapper class is given afterwards.

GlassFish

The one server where the role mapper was simple to obtain was GlassFish. The code how to do this is clearly visible in the in-memory example JACC provider that ships with GlassFish. A small confusing thing is that the example class and its interface contain many methods that aren't actually used. Based on this example the reflective code and mapping became as follows:

private boolean tryGlassFish(String contextID, Collection<String> allDeclaredRoles) {

    try {
        Class<?> SecurityRoleMapperFactoryClass = Class.forName("org.glassfish.deployment.common.SecurityRoleMapperFactory");

        Object factoryInstance = Class.forName("org.glassfish.internal.api.Globals")
                                      .getMethod("get", SecurityRoleMapperFactoryClass.getClass())
                                      .invoke(null, SecurityRoleMapperFactoryClass);

        Object securityRoleMapperInstance = SecurityRoleMapperFactoryClass.getMethod("getRoleMapper", String.class)
                                                                          .invoke(factoryInstance, contextID);

        @SuppressWarnings("unchecked")
        Map<String, Subject> roleToSubjectMap = (Map<String, Subject>) Class.forName("org.glassfish.deployment.common.SecurityRoleMapper")
                                                                            .getMethod("getRoleToSubjectMapping")
                                                                            .invoke(securityRoleMapperInstance);

        for (String role : allDeclaredRoles) {
            if (roleToSubjectMap.containsKey(role)) {
                Set<Principal> principals = roleToSubjectMap.get(role).getPrincipals();

                List<String> groups = getGroupsFromPrincipals(principals);
                for (String group : groups) {
                    if (!groupToRoles.containsKey(group)) {
                        groupToRoles.put(group, new ArrayList<String>());
                    }
                    groupToRoles.get(group).add(role);
                }

                if ("**".equals(role) && !groups.isEmpty()) {
                    // JACC spec 3.2 states:
                    //
                    // "For the any "authenticated user role", "**", and unless an application specific mapping has
                    // been established for this role,
                    // the provider must ensure that all permissions added to the role are granted to any
                    // authenticated user."
                    //
                    // Here we check for the "unless" part mentioned above. If we're dealing with the "**" role here
                    // and groups is not
                    // empty, then there's an application specific mapping and "**" maps only to those groups, not
                    // to any authenticated user.
                    anyAuthenticatedUserRoleMapped = true;
                }
            }
        }

        return true;

    } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
            | InvocationTargetException e) {
        return false;
    }
}

Finding out how to install the JACC provider took a bit more time. For some reason the documentation doesn't mention it, but the location to put the mentioned jar file is simply:

[glassfish_home]/glassfish/lib
GlassFish has a convenience mechanism to put a named JACC configuration in the following file:
[glassfish_home]/glassfish/domains1/domain1/config/domain.xml
This name has to be added to the security-config element and a jacc-provider element that specifies both the policy and factory classes as follows:
<security-service jacc="test">
    <!-- Other elements here -->
    <jacc-provider policy-provider="test.TestPolicy" name="test" policy-configuration-factory-provider="test.TestPolicyConfigurationFactory"></jacc-provider>
</security-service>

WebLogic

WebLogic turned out to be a great deal more difficult than GlassFish. Being closed source you can't just look into any default JACC provider, but as it happens the WebLogic documentation mentioned (actually, requires) a pluggable role mapper:

-Dweblogic.security.jacc.RoleMapperFactory.provider=weblogic.security.jacc.simpleprovider.RoleMapperFactoryImpl
Unfortunately, even though an option for a role mapper factory class is used, there's no documentation on what one's own role mapper factory should do (which interfaces it should implement, which interfaces the actual role mapper it returns should implement etc).

After a fair amount of Googling I did eventually found that what appears to be a super class is documented. Furthermore, the interface of a type called RoleMapper is documented as well.

Unfortunately that last interface does not contain any of the actual methods to do role mapping, so you can't use an implementation of just this. This all was really surprising; WebLogic gives the option to specify a role mapper factory, but key details are missing. Still, the above gave just enough hints to do some reflective experiments, and after a lot of trial and error I came to the following code that seemed to do the trick:

private boolean tryWebLogic(String contextID, Collection<String> allDeclaredRoles) {

    try {

        // See http://docs.oracle.com/cd/E21764_01/apirefs.1111/e13941/weblogic/security/jacc/RoleMapperFactory.html
        Class<?> roleMapperFactoryClass = Class.forName("weblogic.security.jacc.RoleMapperFactory");

        // RoleMapperFactory implementation class always seems to be the value of what is passed on the commandline
        // via the -Dweblogic.security.jacc.RoleMapperFactory.provider option.
        // See http://docs.oracle.com/cd/E57014_01/wls/SCPRG/server_prot.htm
        Object roleMapperFactoryInstance = roleMapperFactoryClass.getMethod("getRoleMapperFactory")
                                                                 .invoke(null);

        // See http://docs.oracle.com/cd/E21764_01/apirefs.1111/e13941/weblogic/security/jacc/RoleMapperFactory.html#getRoleMapperForContextID(java.lang.String)
        Object roleMapperInstance = roleMapperFactoryClass.getMethod("getRoleMapperForContextID", String.class)
                                                          .invoke(roleMapperFactoryInstance, contextID);

        // This seems really awkward; the Map contains BOTH group names and user names, without ANY way to
        // distinguish between the two.
        // If a user now has a name that happens to be a role as well, we have an issue :X
        @SuppressWarnings("unchecked")
        Map<String, String[]> roleToPrincipalNamesMap = (Map<String, String[]>) Class.forName("weblogic.security.jacc.simpleprovider.RoleMapperImpl")
                                                                                     .getMethod("getRolesToPrincipalNames")
                                                                                     .invoke(roleMapperInstance);

        for (String role : allDeclaredRoles) {
            if (roleToPrincipalNamesMap.containsKey(role)) {

                List<String> groupsOrUserNames = asList(roleToPrincipalNamesMap.get(role));

                for (String groupOrUserName : roleToPrincipalNamesMap.get(role)) {
                    // Ignore the fact that the collection also contains user names and hope
                    // that there are no user names in the application with the same name as a group
                    if (!groupToRoles.containsKey(groupOrUserName)) {
                        groupToRoles.put(groupOrUserName, new ArrayList<String>());
                    }
                    groupToRoles.get(groupOrUserName).add(role);
                }

                if ("**".equals(role) && !groupsOrUserNames.isEmpty()) {
                    // JACC spec 3.2 states: [...]
                    anyAuthenticatedUserRoleMapped = true;
                }
            }
        }

        return true;

    } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
            | InvocationTargetException e) {
        return false;
    }
}

Adding the two standard system properties for WebLogic appeared to be done most conveniently in the file:

[wls_home]/user_projects/domains/mydomain/bin/setDomainEnv.sh
There's a comment in the file that says to uncomment a section to use JACC, but that however is completely wrong. If you do indeed uncomment it, the server will not start: it are a few -D options, each on the beginning of a line, but at that point in the file you can't specify -D options that way. Furthermore it suggests that it's required to activate the Java SE security manager, but LUCKILY this is NOT the case. From WebLogic 12.1.3 onwards the security manager is no longer required (which is a huge win for working with JACC on WebLogic). The following does work though for our own JACC provider:
JACC_PROPERTIES="-Djavax.security.jacc.policy.provider=test.TestPolicy  -Djavax.security.jacc.PolicyConfigurationFactory.provider=test.TestPolicyConfigurationFactory -Dweblogic.security.jacc.RoleMapperFactory.provider=weblogic.security.jacc.simpleprovider.RoleMapperFactoryImpl "

JAVA_PROPERTIES="${JAVA_PROPERTIES} ${EXTRA_JAVA_PROPERTIES} ${JACC_PROPERTIES}"
export JAVA_PROPERTIES
For completeness and future reference, the following definition for JACC_PROPERTIES activates the provided JACC provider:
# JACC_PROPERTIES="-Djavax.security.jacc.policy.provider=weblogic.security.jacc.simpleprovider.SimpleJACCPolicy -Djavax.security.jacc.PolicyConfigurationFactory.provider=weblogic.security.jacc.simpleprovider.PolicyConfigurationFactoryImpl -Dweblogic.security.jacc.RoleMapperFactory.provider=weblogic.security.jacc.simpleprovider.RoleMapperFactoryImpl "
(Do note that WebLogic violates the Java EE spec here. Such activation should NOT be needed, as a JACC provider should be active by default.)

The location of where to put the JACC provider jar was not as straightforward. I tried the [wls_home]/user_projects/domains/mydomain/lib] folder, and although WebLogic did seem to detect "something" here as it would log during startup that it encountered a library and was adding it, it would not actually work and class not found exceptions followed. After some fiddling I got around this by adding the following at the point the CLASSPATH variable is exported:

CLASSPATH="${DOMAIN_HOME}/lib/jacctest-0.0.1-SNAPSHOT.jar:${CLASSPATH}"
export CLASSPATH
I'm not sure if this is the recommended approach, but it seemed to do the trick.

Geronimo

Where WebLogic was a great deal more difficult than GlassFish, Geronimo unfortunately was extremely more difficult. In 2 decades of working with a variety of platforms and languages I think getting this to work ranks pretty high on the list of downright bizarre things that are required to get something to work. The only thing that comes close is getting some obscure undocumented activeX control to work in a C++ Windows app around 1997.

The role mapper in Geronimo is not directly accessibly via some factory or service as in GlassFish and WebLogic, but instead there's a map containing the mapping, which is injected in a Geronimo specific JACC provider that extends something and implements many interfaces. As we obviously don't have or want to have a Geronimo specific provider I tried to find out how this injection exactly works.

Things start with a class called GeronimoSecurityBuilderImpl that parses the XML that expresses the role mapping. Nothing too obscure here. This class then registers a so-called GBean (a kind of Geronimo specific JMX bean) that it passes the previously mentioned Map, and then registers a second GBean that it gives a reference to this first GBean. Meanwhile, the Geronimo specific policy configuration factory, called GeronimoPolicyConfigurationFactory "registers" itself via a static method on one of the GBeans mentioned before. Those GBeans at some point start running, and use the factory that was set by the static method to get a Geronimo specific policy configuration and then call a method on that to pass the Map containing the role mapping.

Now this scheme is not only rather convoluted to say the least, there's also no way to get to this map from anywhere else without resorting to very ugly hacks and using reflection to hack into private instance variables. It was possible to programmatically obtain a GBean, but the one we're after has many instances and it didn't prove easy to get the one that applies to the current web app. There seemed to be an option if you know the maven-like coordinates of your own app, but I didn't wanted to hardcode these and didn't found an API to obtain those programmatically. Via the source I noticed another way was via some meta data about a GBean, but there was no API available to obtain this.

After spending far more hours than willing to admit, I finally came to the following code to obtain the Map I was after:

private void tryGeronimoAlternative() {
    Kernel kernel = KernelRegistry.getSingleKernel();
    
    try {
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        
        Field registryField = kernel.getClass().getDeclaredField("registry");
        registryField.setAccessible(true);
        BasicRegistry registry = (BasicRegistry) registryField.get(kernel);
        
        Set<GBeanInstance> instances = registry.listGBeans(new AbstractNameQuery(null, Collections.EMPTY_MAP, ApplicationPrincipalRoleConfigurationManager.class.getName()));
        
        Map<Principal, Set<String>> principalRoleMap = null;
        for (GBeanInstance instance : instances) {
            
            Field classLoaderField = instance.getClass().getDeclaredField("classLoader");
            classLoaderField.setAccessible(true);
            ClassLoader gBeanClassLoader = (ClassLoader) classLoaderField.get(instance);
            
            if (gBeanClassLoader.equals(contextClassLoader)) {
                
                ApplicationPrincipalRoleConfigurationManager manager = (ApplicationPrincipalRoleConfigurationManager) instance.getTarget();
                Field principalRoleMapField = manager.getClass().getDeclaredField("principalRoleMap");
                principalRoleMapField.setAccessible(true);
                
                principalRoleMap = (Map<Principal, Set<String>>) principalRoleMapField.get(manager);
                break;
                
            }
            
            // process principalRoleMap here
           
        }
        
    } catch (InternalKernelException | IllegalStateException | NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e1) {
        // Ignore
    }
}
Note that this is the "raw" code and not yet converted to be fully reflection based like the GlassFish and WebLogic examples and not is not yet converting the principalRoleMap to the uniform format we use.

In order to install the custom JACC provider I looked for a config file or startup script, but there didn't seem to be an obvious one. So I just supplied the standardized options directly on the command line as follows:

-Djavax.security.jacc.policy.provider=test.TestPolicy  
-Djavax.security.jacc.PolicyConfigurationFactory.provider=test.TestPolicyConfigurationFactory
I then tried to find a place to put the jar again, but simply couldn't find one. There just doesn't seem to be any mechanism to extend Geronimo's class path for the entire server, which is (perhaps unfortunately) what JACC needs. There were some options for individual deployments, but this cannot work for JACC since the Policy instance is called at a very low level and for everything that is deployed on the server. Geronimo by default deploys about 10 applications for all kinds of things. Mocking with each and every one of them just isn't feasible.

What I eventually did is perhaps one of the biggest hacks ever; I injected the required classes directly into the Geronimo library that contains the default JACC provider. After all, this provider is already used, so surely Geronimo has to be able to load my custom provider from THIS location :X

All libraries in Geronimo are OSGI bundles, so in addition to just injecting my classes I also had to adjust the MANIFEST, but after doing that Geronimo was FINALLY able to find my custom JACC provider. The MANIFEST was updated by copying the existing one from the jar and adding the following to it:

test;uses:="org.apa
 che.geronimo.security.jaspi,javax.security.auth,org.apache.geronimo.s
 ecurity,org.apache.geronimo.security.realm.providers,org.apache.geron
 imo.security.jaas,javax.security.auth.callback,javax.security.auth.lo
 gin,javax.security.auth.message.callback"
And then running the zip command as follows:
zip /test/geronimo-tomcat7-javaee6-3.0.1/repository/org/apache/geronimo/framework/geronimo-security/3.0.1/geronimo-security-3.0.1.jar META-INF/MANIFEST.MF 
From the root directory where my compiled classes live I executed the following command to inject them:
jar uf /test/geronimo-tomcat7-javaee6-3.0.1/repository/org/apache/geronimo/framework/geronimo-security/3.0.1/geronimo-security-3.0.1.jar test/*
I happily admit it's pretty insane to do it like this. Hopefully this is not really the way to do it, and there's a sane way that I just happened to miss, or that someone with deep Geronimo knowledge would "just know".

Much to my dismay, the absurdity didn't end there. As it appears the previously mentioned GBeans act as a kind of protection mechanism to ensure only Geronimo specific JACC providers are installed. Since the entire purpose of the exercise is to install a general universal JACC provider, turning it into a Geronimo specific one obviously wasn't an option. The scarce documentation vaguely hints at replacing some of these GBeans or the security builder specifically for your application, but since JACC is installed for the entire server this just isn't feasible.

Eventually I tricked Geronimo into thinking a Geronimo specific JACC provider was installed by instantiating (via reflection) a dummy Geronimo policy provider factory and putting intercepting proxies into it to prevent a NPE that would otherwise ensue. As a side effect of this hack to beat Geronimo's "protection" I could capture the map I previously grabbed via reflective hacks somewhat easier.

The code to install the dummy factory:

try {
    // Geronimo 3.0.1 contains a protection mechanism to ensure only a Geronimo policy provider is installed.
    // This protection can be beat by creating an instance of GeronimoPolicyConfigurationFactory once. This instance
    // will statically register itself with an internal Geronimo class
    geronimoPolicyConfigurationFactoryInstance = Class.forName("org.apache.geronimo.security.jacc.mappingprovider.GeronimoPolicyConfigurationFactory").newInstance();
    geronimoContextToRoleMapping = new ConcurrentHashMap<>();
} catch (Exception e) {
    // ignore
}
The code to put the capturing policy configurations in place:
// Are we dealing with Geronimo?
if (geronimoPolicyConfigurationFactoryInstance != null) {
    
    // PrincipalRoleConfiguration
    
    try {
        Class<?> geronimoPolicyConfigurationClass = Class.forName("org.apache.geronimo.security.jacc.mappingprovider.GeronimoPolicyConfiguration");
        
        Object geronimoPolicyConfigurationProxy = Proxy.newProxyInstance(TestRoleMapper.class.getClassLoader(), new Class[] {geronimoPolicyConfigurationClass}, new InvocationHandler() {
            
            @SuppressWarnings("unchecked")
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                // Take special action on the following method:
                
                // void setPrincipalRoleMapping(Map<Principal, Set<String>> principalRoleMap) throws PolicyContextException;
                if (method.getName().equals("setPrincipalRoleMapping")) {
                    
                    geronimoContextToRoleMapping.put(contextID, (Map<Principal, Set<String>>) args[0]);
                    
                }
                return null;
            }
        });
        
        // Set the proxy on the GeronimoPolicyConfigurationFactory so it will call us back later with the role mapping via the following method:
        
        // public void setPolicyConfiguration(String contextID, GeronimoPolicyConfiguration configuration) {
        Class.forName("org.apache.geronimo.security.jacc.mappingprovider.GeronimoPolicyConfigurationFactory")
             .getMethod("setPolicyConfiguration", String.class, geronimoPolicyConfigurationClass)
             .invoke(geronimoPolicyConfigurationFactoryInstance, contextID, geronimoPolicyConfigurationProxy);
        
        
    } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
        // Ignore
    }
}
And finally the code to transform the map into our uniform target map:
private boolean tryGeronimo(String contextID, Collection<String> allDeclaredRoles) {
    if (geronimoContextToRoleMapping != null) {
        
        if (geronimoContextToRoleMapping.containsKey(contextID)) {
            Map<Principal, Set<String>> principalsToRoles = geronimoContextToRoleMapping.get(contextID);
            
            for (Map.Entry<Principal, Set<String>> entry : principalsToRoles.entrySet()) {
                
                // Convert the principal that's used as the key in the Map to a list of zero or more groups.
                // (for Geronimo we know that using the default role mapper it's always zero or one group)
                for (String group : principalToGroups(entry.getKey())) {
                    if (!groupToRoles.containsKey(group)) {
                        groupToRoles.put(group, new ArrayList<String>());
                    }
                    groupToRoles.get(group).addAll(entry.getValue());
                    
                    if (entry.getValue().contains("**")) {
                        // JACC spec 3.2 states: [...]
                        anyAuthenticatedUserRoleMapped = true;
                    }
                }
            }
        }
        
        return true;
    }
    
    return false;
}

The role mapper class

After having taken a look at the code for each individual server in isolation above, it's now time to show the full code for the RoleMapper class. This is the class that the JACC provider that we'll present in the next part will use as the universal way to obtain the server's role mapping, as-if this was already standardized:

package test;

import static java.util.Arrays.asList;
import static java.util.Collections.list;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.security.Principal;
import java.security.acl.Group;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.security.auth.Subject;

public class TestRoleMapper {
    
    private static Object geronimoPolicyConfigurationFactoryInstance;
    private static ConcurrentMap<String, Map<Principal, Set<String>>> geronimoContextToRoleMapping;
    
    private Map<String, List<String>> groupToRoles = new HashMap<>();

    private boolean oneToOneMapping;
    private boolean anyAuthenticatedUserRoleMapped = false;
    
    public static void onFactoryCreated() {
        tryInitGeronimo();
    }
    
    private static void tryInitGeronimo() {
        try {
            // Geronimo 3.0.1 contains a protection mechanism to ensure only a Geronimo policy provider is installed.
            // This protection can be beat by creating an instance of GeronimoPolicyConfigurationFactory once. This instance
            // will statically register itself with an internal Geronimo class
            geronimoPolicyConfigurationFactoryInstance = Class.forName("org.apache.geronimo.security.jacc.mappingprovider.GeronimoPolicyConfigurationFactory").newInstance();
            geronimoContextToRoleMapping = new ConcurrentHashMap<>();
        } catch (Exception e) {
            // ignore
        }
    }
    
    public static void onPolicyConfigurationCreated(final String contextID) {
        
        // Are we dealing with Geronimo?
        if (geronimoPolicyConfigurationFactoryInstance != null) {
            
            // PrincipalRoleConfiguration
            
            try {
                Class<?> geronimoPolicyConfigurationClass = Class.forName("org.apache.geronimo.security.jacc.mappingprovider.GeronimoPolicyConfiguration");
                
                Object geronimoPolicyConfigurationProxy = Proxy.newProxyInstance(TestRoleMapper.class.getClassLoader(), new Class[] {geronimoPolicyConfigurationClass}, new InvocationHandler() {
                    
                    @SuppressWarnings("unchecked")
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        // Take special action on the following method:
                        
                        // void setPrincipalRoleMapping(Map<Principal, Set<String>> principalRoleMap) throws PolicyContextException;
                        if (method.getName().equals("setPrincipalRoleMapping")) {
                            
                            geronimoContextToRoleMapping.put(contextID, (Map<Principal, Set<String>>) args[0]);
                            
                        }
                        return null;
                    }
                });
                
                // Set the proxy on the GeronimoPolicyConfigurationFactory so it will call us back later with the role mapping via the following method:
                
                // public void setPolicyConfiguration(String contextID, GeronimoPolicyConfiguration configuration) {
                Class.forName("org.apache.geronimo.security.jacc.mappingprovider.GeronimoPolicyConfigurationFactory")
                     .getMethod("setPolicyConfiguration", String.class, geronimoPolicyConfigurationClass)
                     .invoke(geronimoPolicyConfigurationFactoryInstance, contextID, geronimoPolicyConfigurationProxy);
                
                
            } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                // Ignore
            }
        }
    }
    

    public TestRoleMapper(String contextID, Collection<String> allDeclaredRoles) {
        // Initialize the groupToRoles map

        // Try to get a hold of the proprietary role mapper of each known
        // AS. Sad that this is needed :(
        if (tryGlassFish(contextID, allDeclaredRoles)) {
            return;
        } else if (tryWebLogic(contextID, allDeclaredRoles)) {
            return;
        } else if (tryGeronimo(contextID, allDeclaredRoles)) {
            return;
        } else {
            oneToOneMapping = true;
        }
    }

    public List<String> getMappedRolesFromPrincipals(Principal[] principals) {
        return getMappedRolesFromPrincipals(asList(principals));
    }

    public boolean isAnyAuthenticatedUserRoleMapped() {
        return anyAuthenticatedUserRoleMapped;
    }

    public List<String> getMappedRolesFromPrincipals(Iterable<Principal> principals) {

        // Extract the list of groups from the principals. These principals typically contain
        // different kind of principals, some groups, some others. The groups are unfortunately vendor
        // specific.
        List<String> groups = getGroupsFromPrincipals(principals);

        // Map the groups to roles. E.g. map "admin" to "administrator". Some servers require this.
        return mapGroupsToRoles(groups);
    }

    private List<String> mapGroupsToRoles(List<String> groups) {

        if (oneToOneMapping) {
            // There is no mapping used, groups directly represent roles.
            return groups;
        }

        List<String> roles = new ArrayList<>();

        for (String group : groups) {
            if (groupToRoles.containsKey(group)) {
                roles.addAll(groupToRoles.get(group));
            }
        }

        return roles;
    }

    private boolean tryGlassFish(String contextID, Collection<String> allDeclaredRoles) {

        try {
            Class<?> SecurityRoleMapperFactoryClass = Class.forName("org.glassfish.deployment.common.SecurityRoleMapperFactory");

            Object factoryInstance = Class.forName("org.glassfish.internal.api.Globals")
                                          .getMethod("get", SecurityRoleMapperFactoryClass.getClass())
                                          .invoke(null, SecurityRoleMapperFactoryClass);

            Object securityRoleMapperInstance = SecurityRoleMapperFactoryClass.getMethod("getRoleMapper", String.class)
                                                                              .invoke(factoryInstance, contextID);

            @SuppressWarnings("unchecked")
            Map<String, Subject> roleToSubjectMap = (Map<String, Subject>) Class.forName("org.glassfish.deployment.common.SecurityRoleMapper")
                                                                                .getMethod("getRoleToSubjectMapping")
                                                                                .invoke(securityRoleMapperInstance);

            for (String role : allDeclaredRoles) {
                if (roleToSubjectMap.containsKey(role)) {
                    Set<Principal> principals = roleToSubjectMap.get(role).getPrincipals();

                    List<String> groups = getGroupsFromPrincipals(principals);
                    for (String group : groups) {
                        if (!groupToRoles.containsKey(group)) {
                            groupToRoles.put(group, new ArrayList<String>());
                        }
                        groupToRoles.get(group).add(role);
                    }

                    if ("**".equals(role) && !groups.isEmpty()) {
                        // JACC spec 3.2 states:
                        //
                        // "For the any "authenticated user role", "**", and unless an application specific mapping has
                        // been established for this role,
                        // the provider must ensure that all permissions added to the role are granted to any
                        // authenticated user."
                        //
                        // Here we check for the "unless" part mentioned above. If we're dealing with the "**" role here
                        // and groups is not
                        // empty, then there's an application specific mapping and "**" maps only to those groups, not
                        // to any authenticated user.
                        anyAuthenticatedUserRoleMapped = true;
                    }
                }
            }

            return true;

        } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
                | InvocationTargetException e) {
            return false;
        }
    }

    private boolean tryWebLogic(String contextID, Collection<String> allDeclaredRoles) {

        try {

            // See http://docs.oracle.com/cd/E21764_01/apirefs.1111/e13941/weblogic/security/jacc/RoleMapperFactory.html
            Class<?> roleMapperFactoryClass = Class.forName("weblogic.security.jacc.RoleMapperFactory");

            // RoleMapperFactory implementation class always seems to be the value of what is passed on the commandline
            // via the -Dweblogic.security.jacc.RoleMapperFactory.provider option.
            // See http://docs.oracle.com/cd/E57014_01/wls/SCPRG/server_prot.htm
            Object roleMapperFactoryInstance = roleMapperFactoryClass.getMethod("getRoleMapperFactory")
                                                                     .invoke(null);

            // See http://docs.oracle.com/cd/E21764_01/apirefs.1111/e13941/weblogic/security/jacc/RoleMapperFactory.html#getRoleMapperForContextID(java.lang.String)
            Object roleMapperInstance = roleMapperFactoryClass.getMethod("getRoleMapperForContextID", String.class)
                                                              .invoke(roleMapperFactoryInstance, contextID);

            // This seems really awkward; the Map contains BOTH group names and user names, without ANY way to
            // distinguish between the two.
            // If a user now has a name that happens to be a role as well, we have an issue :X
            @SuppressWarnings("unchecked")
            Map<String, String[]> roleToPrincipalNamesMap = (Map<String, String[]>) Class.forName("weblogic.security.jacc.simpleprovider.RoleMapperImpl")
                                                                                         .getMethod("getRolesToPrincipalNames")
                                                                                         .invoke(roleMapperInstance);

            for (String role : allDeclaredRoles) {
                if (roleToPrincipalNamesMap.containsKey(role)) {

                    List<String> groupsOrUserNames = asList(roleToPrincipalNamesMap.get(role));

                    for (String groupOrUserName : roleToPrincipalNamesMap.get(role)) {
                        // Ignore the fact that the collection also contains user names and hope
                        // that there are no user names in the application with the same name as a group
                        if (!groupToRoles.containsKey(groupOrUserName)) {
                            groupToRoles.put(groupOrUserName, new ArrayList<String>());
                        }
                        groupToRoles.get(groupOrUserName).add(role);
                    }

                    if ("**".equals(role) && !groupsOrUserNames.isEmpty()) {
                        // JACC spec 3.2 states: [...]
                        anyAuthenticatedUserRoleMapped = true;
                    }
                }
            }

            return true;

        } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
                | InvocationTargetException e) {
            return false;
        }
    }
    
    private boolean tryGeronimo(String contextID, Collection<String> allDeclaredRoles) {
        if (geronimoContextToRoleMapping != null) {
            
            if (geronimoContextToRoleMapping.containsKey(contextID)) {
                Map<Principal, Set<String>> principalsToRoles = geronimoContextToRoleMapping.get(contextID);
                
                for (Map.Entry<Principal, Set<String>> entry : principalsToRoles.entrySet()) {
                    
                    // Convert the principal that's used as the key in the Map to a list of zero or more groups.
                    // (for Geronimo we know that using the default role mapper it's always zero or one group)
                    for (String group : principalToGroups(entry.getKey())) {
                        if (!groupToRoles.containsKey(group)) {
                            groupToRoles.put(group, new ArrayList<String>());
                        }
                        groupToRoles.get(group).addAll(entry.getValue());
                        
                        if (entry.getValue().contains("**")) {
                            // JACC spec 3.2 states: [...]
                            anyAuthenticatedUserRoleMapped = true;
                        }
                    }
                }
            }
            
            return true;
        }
        
        return false;
    }

    /**
     * Extracts the roles from the vendor specific principals. SAD that this is needed :(
     * 
     * @param principals
     * @return
     */
    public List<String> getGroupsFromPrincipals(Iterable<Principal> principals) {
        List<String> groups = new ArrayList<>();

        for (Principal principal : principals) {
            if (principalToGroups(principal, groups)) {
                // return value of true means we're done early. This can be used
                // when we know there's only 1 principal holding all the groups
                return groups;
            }
        }

        return groups;
    }
    
    public List<String> principalToGroups(Principal principal) {
        List<String> groups = new ArrayList<>();
        principalToGroups(principal, groups);
        return groups;
    }
    
    public boolean principalToGroups(Principal principal, List<String> groups) {
        switch (principal.getClass().getName()) {

            case "org.glassfish.security.common.Group": // GlassFish
            case "org.apache.geronimo.security.realm.providers.GeronimoGroupPrincipal": // Geronimo
            case "weblogic.security.principal.WLSGroupImpl": // WebLogic
            case "jeus.security.resource.GroupPrincipalImpl": // JEUS
                groups.add(principal.getName());
                break;
    
            case "org.jboss.security.SimpleGroup": // JBoss
                if (principal.getName().equals("Roles") && principal instanceof Group) {
                    Group rolesGroup = (Group) principal;
                    for (Principal groupPrincipal : list(rolesGroup.members())) {
                        groups.add(groupPrincipal.getName());
                    }
    
                    // Should only be one group holding the roles, so can exit the loop
                    // early
                    return true;
                }
            }
        return false;
    }

}

Server mapping overview

Each server essentially provides the same core data; a role to group mapping, but each server puts this data in a different format. The table below summarizes this:

Role to group format per server
ServerMapKeyValue
GlassFish 4.1Map<String, Subject>Role nameSubject containing Principals representing groups and users (different class type for each)
WebLogic 12.1.3Map<String, String[]>Role nameGroups and user names (impossible to distinguish which is which)
Geronimo 3.0.1Map<Principal, Set<String>>Principal representing group or user (different class type for each)Role names

As we can see above, GlassFish and WebLogic both have a "a role name to groups and users" format. In the case of GlassFish the groups and users are for some reason wrapped in a Subject. A Map<String, Set<Principal>> would perhaps have been more logical here. WebLogic unfortunately uses a String to represent both group- and user names, meaning there's no way to know if a given name represents a group or a user. One can only guess at what the idea behind this design decision must have been.

Geronimo finally does the mapping exactly the other way around; it has a "group or user to role names" format. After all the insanity we saw with Geronimo this actually is a fairly sane mapping.

Conclusion

As we saw obtaining the container specific role mapping for a universal JACC provider is no easy feat. Finding out how to deploy a JACC provider appeared to be surprisingly difficult, and in case of Geronimo even nearly impossible. It's hard to say what can be done to improve this. Should JACC define an extra standardized property where you can provide the path to a jar file? E.g. something like
-Djavax.security.jacc.provider.jar=/usr/lib/myprovider.jar
At least for testing, and probably for regular usage as well, it would be extremely convenient if JACC providers could additionally be registered from within an application archive.

Arjan Tijms

Comments

Popular posts from this blog

Implementing container authentication in Java EE with JASPIC

What’s new in Jakarta Security 3?

Jakarta EE Survey 2022