Thursday, March 19, 2015

Java EE authorization - JACC revisited part III

This is the third and final part of a series where we revisit JACC after taking an initial look at it last year.

In the first part we mainly looked at various role mapping strategies, while the main topic of the second part was obtaining the container specific role mapper and the container specific way of how a JACC provider is deployed.

In this third and final part we'll be bringing it all together and present a fully working JACC provider for a single application module (e.g. a single war).

Architecture

As explained before, implementing a JACC provider requires implementing three classes:

  1. PolicyConfigurationFactory
  2. PolicyConfiguration
  3. Policy
Zooming into these, the following is what is more accurately required to be implemented:
  1. A factory that provides an object that collects permissions
  2. A state machine that controls the life-cyle of this permission collector
  3. Linking permissions of multiple modules and utilities
  4. Collecting and managing permissions
  5. Processing permissions after collecting
  6. An "authorization module" using permissions for authorization decisions

In the implementation given before we put all this functionality in the specified three classes. Here we'll split out each item to a separate class (we'll skip linking though, which is only required for EARs where security constraints are defined in multiple modules). This will result in more classes in total, but each class is hopefully easier to understand.

A factory that provides an object that collects permissions

The factory is largely as given earlier, but contains a few fixes and makes use of the state machine that is shown below.

import static javax.security.jacc.PolicyContext.getContextID;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.security.jacc.PolicyConfiguration;
import javax.security.jacc.PolicyConfigurationFactory;
import javax.security.jacc.PolicyContextException;

public class TestPolicyConfigurationFactory extends PolicyConfigurationFactory {
    
    private static final ConcurrentMap<String, TestPolicyConfigurationStateMachine> configurators = new ConcurrentHashMap<>();

    @Override
    public PolicyConfiguration getPolicyConfiguration(String contextID, boolean remove) throws PolicyContextException {
        
        if (!configurators.containsKey(contextID)) {
            configurators.putIfAbsent(contextID, new TestPolicyConfigurationStateMachine(new TestPolicyConfiguration(contextID)));
        }
        
        TestPolicyConfigurationStateMachine testPolicyConfigurationStateMachine = configurators.get(contextID);
        
        if (remove) {
            testPolicyConfigurationStateMachine.delete();
        }
        
        // According to the contract of getPolicyConfiguration() every PolicyConfiguration returned from here
        // should always be transitioned to the OPEN state.
        testPolicyConfigurationStateMachine.open();
        
        return testPolicyConfigurationStateMachine;
    }
    
    @Override
    public boolean inService(String contextID) throws PolicyContextException {
        TestPolicyConfigurationStateMachine testPolicyConfigurationStateMachine = configurators.get(contextID);
        if (testPolicyConfigurationStateMachine == null) {
            return false;
        }
        
        return testPolicyConfigurationStateMachine.inService();
    }
    
    public static TestPolicyConfiguration getCurrentPolicyConfiguration() {
        return (TestPolicyConfiguration) configurators.get(getContextID()).getPolicyConfiguration();
    }
    
}

A state machine that controls the life-cyle of this permission collector

The state machine as required by the spec was left out in the previous example, but we've implemented it now. A possible implementation could have been to actually use a generic state machine that's been given some kind of rules file. Indeed, some implementations take this approach. But as the rules are actually not that complicated and there are not much transitions to speak of I found that just providing a few checks was a much easier method.

A class such as this would perhaps better be provided by the container, as it seems unlikely individual PolicyConfigurations would often if ever need to do anything specific here.

import static test.TestPolicyConfigurationStateMachine.State.DELETED;
import static test.TestPolicyConfigurationStateMachine.State.INSERVICE;
import static test.TestPolicyConfigurationStateMachine.State.OPEN;

import java.security.Permission;
import java.security.PermissionCollection;

import javax.security.jacc.PolicyConfiguration;
import javax.security.jacc.PolicyConfigurationFactory;
import javax.security.jacc.PolicyContextException;

public class TestPolicyConfigurationStateMachine implements PolicyConfiguration {

    public static enum State {
        OPEN, INSERVICE, DELETED
    };

    private State state = OPEN;
    private PolicyConfiguration policyConfiguration;
    

    public TestPolicyConfigurationStateMachine(PolicyConfiguration policyConfiguration) {
        this.policyConfiguration = policyConfiguration;
    }
    
    public PolicyConfiguration getPolicyConfiguration() {
        return policyConfiguration;
    }

    
    // ### Methods that can be called in any state and don't change state
    
    @Override
    public String getContextID() throws PolicyContextException {
        return policyConfiguration.getContextID();
    }
    
    @Override
    public boolean inService() throws PolicyContextException {
        return state == INSERVICE;
    }
    
    
    // ### Methods where state should be OPEN and don't change state
    
    @Override
    public void addToExcludedPolicy(Permission permission) throws PolicyContextException {
        checkStateIs(OPEN);
        policyConfiguration.addToExcludedPolicy(permission);
    }

    @Override
    public void addToUncheckedPolicy(Permission permission) throws PolicyContextException {
        checkStateIs(OPEN);
        policyConfiguration.addToUncheckedPolicy(permission);
    }

    @Override
    public void addToRole(String roleName, Permission permission) throws PolicyContextException {
        checkStateIs(OPEN);
        policyConfiguration.addToRole(roleName, permission);
    }
    
    @Override
    public void addToExcludedPolicy(PermissionCollection permissions) throws PolicyContextException {
        checkStateIs(OPEN);
        policyConfiguration.addToExcludedPolicy(permissions);
    }
    
    @Override
    public void addToUncheckedPolicy(PermissionCollection permissions) throws PolicyContextException {
        checkStateIs(OPEN);
        policyConfiguration.addToUncheckedPolicy(permissions);
    }
    
    @Override
    public void addToRole(String roleName, PermissionCollection permissions) throws PolicyContextException {
        checkStateIs(OPEN);
        policyConfiguration.addToRole(roleName, permissions);
    }
    
    @Override
    public void linkConfiguration(PolicyConfiguration link) throws PolicyContextException {
        checkStateIs(OPEN);
        policyConfiguration.linkConfiguration(link);
    }
    
    @Override
    public void removeExcludedPolicy() throws PolicyContextException {
        checkStateIs(OPEN);
        policyConfiguration.removeExcludedPolicy();
        
    }

    @Override
    public void removeRole(String roleName) throws PolicyContextException {
        checkStateIs(OPEN);
        policyConfiguration.removeRole(roleName);
    }

    @Override
    public void removeUncheckedPolicy() throws PolicyContextException {
        checkStateIs(OPEN);
        policyConfiguration.removeUncheckedPolicy();
    }
    
    
    // Methods that change the state
    //
    // commit() can only be called when the state is OPEN or INSERVICE and next state is always INSERVICE
    // delete() can always be called and target state will always be DELETED
    // open()   can always be called and target state will always be OPEN
    
    @Override
    public void commit() throws PolicyContextException {
        checkStateIsNot(DELETED);
        
        if (state == OPEN) {
            // Not 100% sure; allow double commit, or ignore double commit?
            // Here we ignore and only call commit on the actual policyConfiguration
            // when the state is OPEN
            policyConfiguration.commit();
            state = INSERVICE;
        }
    }

    @Override
    public void delete() throws PolicyContextException {
        policyConfiguration.delete();
        state = DELETED;
    }
    
    /**
     * Transition back to open. This method is required because of the {@link PolicyConfigurationFactory} contract, but is
     * mysteriously missing from the interface.
     */
    public void open() {
        state = OPEN;
    }
    
    
    // ### Private methods
    
    private void checkStateIs(State requiredState) {
        if (state != requiredState) {
            throw new IllegalStateException("Required status is " + requiredState + " but actual state is " + state);
        }
    }
    
    private void checkStateIsNot(State undesiredState) {
        if (state == undesiredState) {
            throw new IllegalStateException("State could not be " + undesiredState + " but actual state is");
        }
    }

}

Linking permissions of multiple modules and utilities

As mentioned we did not implement linking (perhaps we'll look at this in a future article), but as its an interface method we have to put an (empty) implementation somewhere. At the same time JACC curiously requires us to implement a couple of variations on the permission collection methods that don't even seem to be called in practice by any container we looked at. Finally the PolicyConfiguration interface requires an explicit life-cycle method and an identity method. The life-cycle method is not implemented either since all life-cycle managing is done by the state machine that wraps our actual PolicyConfiguration.

All these "distracting" methods were conveniently shoved into a base class as follows:

import static java.util.Collections.list;

import java.security.Permission;
import java.security.PermissionCollection;

import javax.security.jacc.PolicyConfiguration;
import javax.security.jacc.PolicyContextException;

public abstract class TestPolicyConfigurationBase implements PolicyConfiguration {
    
    private final String contextID;
    
    public TestPolicyConfigurationBase(String contextID) {
        this.contextID = contextID;
    }
    
    @Override
    public String getContextID() throws PolicyContextException {
        return contextID;
    }
    
    @Override
    public void addToExcludedPolicy(PermissionCollection permissions) throws PolicyContextException {
        for (Permission permission : list(permissions.elements())) {
            addToExcludedPolicy(permission);
        }
    }
    
    @Override
    public void addToUncheckedPolicy(PermissionCollection permissions) throws PolicyContextException {
        for (Permission permission : list(permissions.elements())) {
            addToUncheckedPolicy(permission);
        }
    }
    
    @Override
    public void addToRole(String roleName, PermissionCollection permissions) throws PolicyContextException {
        for (Permission permission : list(permissions.elements())) {
            addToRole(roleName, permission);
        }
    }

    @Override
    public void linkConfiguration(PolicyConfiguration link) throws PolicyContextException {
    }
    
    @Override
    public boolean inService() throws PolicyContextException {
        // Not used, taken care of by PolicyConfigurationStateMachine
        return true;
    }

}

Collecting and managing permissions

The next step concerns a base class for a PolicyConfiguration that takes care of the actual collection of permissions, and making those collected permissions available later on. For each permission that the container discovers it calls the appropriate method in this class.

This kind of permission collecting, like the state machine, is actually pretty generic. One wonders if it wouldn't be a great deal simpler if the container just called a single init() method once (or even better, used injection) with a simple data structure containing collections of all permission types. Looking at some container implementations it indeed looks like the container has those collections already and just loops over them handing them one by one to our PolicyConfiguration.

import java.security.Permission;
import java.security.Permissions;
import java.util.HashMap;
import java.util.Map;

import javax.security.jacc.PolicyContextException;

public abstract class TestPolicyConfigurationPermissions extends TestPolicyConfigurationBase {

    private Permissions excludedPermissions = new Permissions();
    private Permissions uncheckedPermissions = new Permissions();
    private Map<String, Permissions> perRolePermissions = new HashMap<String, Permissions>();
    
    public TestPolicyConfigurationPermissions(String contextID) {
        super(contextID);
    }

    @Override
    public void addToExcludedPolicy(Permission permission) throws PolicyContextException {
        excludedPermissions.add(permission);
    }

    @Override
    public void addToUncheckedPolicy(Permission permission) throws PolicyContextException {
        uncheckedPermissions.add(permission);
    }

    @Override
    public void addToRole(String roleName, Permission permission) throws PolicyContextException {
        Permissions permissions = perRolePermissions.get(roleName);
        if (permissions == null) {
            permissions = new Permissions();
            perRolePermissions.put(roleName, permissions);
        }
        
        permissions.add(permission);
    }
    
    @Override
    public void delete() throws PolicyContextException {
        removeExcludedPolicy();
        removeUncheckedPolicy();
        perRolePermissions.clear();
    }

    @Override
    public void removeExcludedPolicy() throws PolicyContextException {
        excludedPermissions = new Permissions();
    }

    @Override
    public void removeRole(String roleName) throws PolicyContextException {
        if (perRolePermissions.containsKey(roleName)) {
            perRolePermissions.remove(roleName);
        } else if ("*".equals(roleName)) {
            perRolePermissions.clear();
        }
    }

    @Override
    public void removeUncheckedPolicy() throws PolicyContextException {
        uncheckedPermissions = new Permissions();
    }
    
    public Permissions getExcludedPermissions() {
        return excludedPermissions;
    }

    public Permissions getUncheckedPermissions() {
        return uncheckedPermissions;
    }

    public Map<String, Permissions> getPerRolePermissions() {
        return perRolePermissions;
    }

}

Processing permissions after collecting

The final part of the PolicyConfiguration concerns a kind of life cycle method again, namely a method that the container calls to indicate all permissions have been handed over to the PolicyConfiguration. In a more modern implementation this might have been an @PostConstruct annotated method.

Contrary to most methods of the PolicyConfiguration that we've seen until now, what happens here is pretty specific to the custom policy provider. Some implementations do a lot of work here and generate a .policy file in the standard Java SE format and write that to disk. This file is then intended to be read back by a standard Java SE Policy implementation.

Other implementations use this moment to optimize the collected permissions by transforming them into their own internal data structure.

In our case we keep the permissions as we collected them and just instantiate a role mapper implementation at this point. The full set of roles that are associated with permissions that each depend on a certain role are passed into the role mapper.

import javax.security.jacc.PolicyContextException;

public class TestPolicyConfiguration extends TestPolicyConfigurationPermissions {

    public TestPolicyConfiguration(String contextID) {
        super(contextID);
    }
    
    private TestRoleMapper roleMapper;

    @Override
    public void commit() throws PolicyContextException {
        roleMapper = new TestRoleMapper(getContextID(), getPerRolePermissions().keySet());
    }
    
    public TestRoleMapper getRoleMapper() {
        return roleMapper;
    }

}
The role mapper referenced in the code shown above was presented in part II of this article and didn't change between parts, but for completeness we'll present it here again:
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;
    }

}

An "authorization module" using permissions for authorization decisions

At long last we present the actual "authorization module" (called Policy in Java SE and JACC). Compared to the version we presented before this now delegates extracting the list of roles from the principles that are associated with the authenticated user to the role mapper we showed above. In addition to that we also added the case where we check for the so-called "any authenticated user", which means it doesn't matter which roles a user has, but only the fact if this user is authenticated or not counts.

This authorization module implements the default authorization algorithm defined by the Servlet and JACC specs, which does the following checks in order:

  1. Is permission excluded? (nobody can access those)
  2. Is permission unchecked? (everyone can access those)
  3. Is permission granted to every authenticated user?
  4. Is permission granted to any of the roles the current user is in?
  5. Is permission granted by the previous (if any) authorization module?

The idea of a custom authorization module is often to do something specific authorization wise, so this would be the most likely place to put custom code. In fact, if only this particular class could be injected with the permissions that now have to be collected by our own classes as shown above, then JACC would be massively simplified in one fell swoop.

In that case only this class would be have to be implemented. Even better would be if the default algorithm was also provided in a portable way. With that we could potentially only implement the parts that are really different for our custom implementation and leave the rest to the default implementation.

import static java.util.Arrays.asList;
import static java.util.Collections.list;
import static test.TestPolicyConfigurationFactory.getCurrentPolicyConfiguration;

import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.Policy;
import java.security.Principal;
import java.security.ProtectionDomain;
import java.util.List;
import java.util.Map;

public class TestPolicy extends Policy {
    
    private Policy previousPolicy = Policy.getPolicy();
    
    @Override
    public boolean implies(ProtectionDomain domain, Permission permission) {
            
        TestPolicyConfiguration policyConfiguration = getCurrentPolicyConfiguration();
        TestRoleMapper roleMapper = policyConfiguration.getRoleMapper();
    
        if (isExcluded(policyConfiguration.getExcludedPermissions(), permission)) {
            // Excluded permissions cannot be accessed by anyone
            return false;
        }
        
        if (isUnchecked(policyConfiguration.getUncheckedPermissions(), permission)) {
            // Unchecked permissions are free to be accessed by everyone
            return true;
        }
        
        List<Principal> currentUserPrincipals = asList(domain.getPrincipals());
        
        if (!roleMapper.isAnyAuthenticatedUserRoleMapped() && !currentUserPrincipals.isEmpty()) {
            // The "any authenticated user" role is not mapped, so available to anyone and the current
            // user is assumed to be authenticated (we assume that an unauthenticated user doesn't have any principals
            // whatever they are)
            if (hasAccessViaRole(policyConfiguration.getPerRolePermissions(), "**", permission)) {
                // Access is granted purely based on the user being authenticated (the actual roles, if any, the user has it not important)
                return true;
            }
        }
        
        if (hasAccessViaRoles(policyConfiguration.getPerRolePermissions(), roleMapper.getMappedRolesFromPrincipals(currentUserPrincipals), permission)) {
            // Access is granted via role. Note that if this returns false it doesn't mean the permission is not
            // granted. A role can only grant, not take away permissions.
            return true;
        }
        
        // Access not granted via any of the JACC maintained Permissions. Check the previous (default) policy.
        // Note: this is likely to be called in case it concerns a Java SE type permissions.
        // TODO: Should we not distinguish between JACC and Java SE Permissions at the start of this method? Seems
        //       very unlikely that JACC would ever say anything about a Java SE Permission, or that the Java SE
        //       policy says anything about a JACC Permission. Why are these two systems even combined in the first place?
        if (previousPolicy != null) {
            return previousPolicy.implies(domain, permission);
        }
        
        return false;
    }

    @Override
    public PermissionCollection getPermissions(ProtectionDomain domain) {

        Permissions permissions = new Permissions();
        
        TestPolicyConfiguration policyConfiguration = getCurrentPolicyConfiguration();
        TestRoleMapper roleMapper = policyConfiguration.getRoleMapper();
        
        Permissions excludedPermissions = policyConfiguration.getExcludedPermissions();

        // First get all permissions from the previous (original) policy
        if (previousPolicy != null) {
            collectPermissions(previousPolicy.getPermissions(domain), permissions, excludedPermissions);
        }

        // If there are any static permissions, add those next
        if (domain.getPermissions() != null) {
            collectPermissions(domain.getPermissions(), permissions, excludedPermissions);
        }

        // Thirdly, get all unchecked permissions
        collectPermissions(policyConfiguration.getUncheckedPermissions(), permissions, excludedPermissions);

        // Finally get the permissions for each role *that the current user has*
        //
        // Note that the principles that are put into the ProtectionDomain object are those from the current user.
        // (for a Server application, passing in a Subject would have been more logical, but the Policy class was
        // made for Java SE with code-level security in mind)
        Map<String, Permissions> perRolePermissions = policyConfiguration.getPerRolePermissions();
        for (String role : roleMapper.getMappedRolesFromPrincipals(domain.getPrincipals())) {
            if (perRolePermissions.containsKey(role)) {
                collectPermissions(perRolePermissions.get(role), permissions, excludedPermissions);
            }
        }

        return permissions;
    }
    
    @Override
    public PermissionCollection getPermissions(CodeSource codesource) {

        Permissions permissions = new Permissions();
        
        TestPolicyConfigurationPermissions policyConfiguration = getCurrentPolicyConfiguration();
        Permissions excludedPermissions = policyConfiguration.getExcludedPermissions();

        // First get all permissions from the previous (original) policy
        if (previousPolicy != null) {
            collectPermissions(previousPolicy.getPermissions(codesource), permissions, excludedPermissions);
        }

        // Secondly get the static permissions. Note that there are only two sources possible here, without
        // knowing the roles of the current user we can't check the per role permissions.
        collectPermissions(policyConfiguration.getUncheckedPermissions(), permissions, excludedPermissions);

        return permissions;
    }
    
    private boolean isExcluded(Permissions excludedPermissions, Permission permission) {
        if (excludedPermissions.implies(permission)) {
            return true;
        }
        
        for (Permission excludedPermission : list(excludedPermissions.elements())) {
            if (permission.implies(excludedPermission)) {
                return true;
            }
        }
        
        return false;
    }
    
    private boolean isUnchecked(Permissions uncheckedPermissions, Permission permission) {
        return uncheckedPermissions.implies(permission);
    }
    
    private boolean hasAccessViaRoles(Map<String, Permissions> perRolePermissions, List<String> roles, Permission permission) {
        for (String role : roles) {
            if (hasAccessViaRole(perRolePermissions, role, permission)) {
                return true;
            }
        }
        
        return false;
    }
    
    private boolean hasAccessViaRole(Map<String, Permissions> perRolePermissions, String role, Permission permission) {
        return perRolePermissions.containsKey(role) && perRolePermissions.get(role).implies(permission);
    }
    
    /**
     * Copies permissions from a source into a target skipping any permission that's excluded.
     * 
     * @param sourcePermissions
     * @param targetPermissions
     * @param excludedPermissions
     */
    private void collectPermissions(PermissionCollection sourcePermissions, PermissionCollection targetPermissions, Permissions excludedPermissions) {
        
        boolean hasExcludedPermissions = excludedPermissions.elements().hasMoreElements();
        
        for (Permission permission : list(sourcePermissions.elements())) {
            if (!hasExcludedPermissions || !isExcluded(excludedPermissions, permission)) {
                targetPermissions.add(permission);
            }
        }
    }
    
}

Conclusion

This concludes our three parter on revisiting JACC. In this third and final part we have looked at an actual Policy Provider. We have broken up the implementation into several parts that each focused on a particular responsibility. While the Policy Provider is complete and working (tested on GlassFish, WebLogic and Geronimo) we did not implement module linking yet, so it's with the caveat that it only works within a single war.

To implement another custom Policy Provider many of these parts can probably be re-used as-is and likely only the Policy itself has to customized.

Arjan Tijms

Wednesday, March 11, 2015

The most popular Java EE servers in 2014/2015 according to OmniFaces users

For a little over 3 months (from half of November 2014 to late February 2015) we had a poll on the OmniFaces website asking what AS (Application Server) people used with OmniFaces (people could select multiple servers).

The response was quite overwhelming for our little project; no less than 840 people responded, choosing a grand total of 1108 servers.

The final results are as follows:

Position Server Votes (Percentage)
1 JBoss (AS/EAP/WildFly) 395 (47%)
2 GlassFish 206 (24%)
3 Tomcat/Mojarra/Weld 186 (22%)
4 TomEE 85 (10%)
5 WebSphere 55 (6%)
6 WebLogic 49 (6%)
7 Tomcat/MyFaces/OWB 33 (3%)
8 Jetty/Mojarra/Weld 19 (2%)
9 Geronimo 13 (1%)
10 JEUS 11 (1%)
11 Liberty 9 (1%)
12 Jetty/MyFaces/OWB 9 (1%)
13 JOnAS 8 (0%)
14 NetWeaver 8 (0%)
15 Resin 6 (0%)
16 InforSuite 5 (0%)
17 WebOTX 4 (0%)
18 Interstage AS 4 (0%)
19 (u)Cosminexus 3 (0%)

As can be seen the clear winner here is JBoss, which gets nearly half of all votes and nearly twice the amount of the runner up; GlassFish. Just slightly below GlassFish at number 3 is Tomcat in the specific combination with Mojarra and Weld.

It has be noted that Mojarra & Weld are typically but a small part of a homegrown Java EE stack, which often also includes things like Hibernate, Hibernate-Validations and many more components. For the specific case of OmniFaces however the Servlet, JSF and CDI implementations are what matter most so that's why we specifically included these in the poll. Another homegrown stack based on Tomcat, but using Myfaces and OWB (OpenWebBeans) instead scores significantly lower and ends up at place 7.

We acknowledge that people not necessarily have to use Mojarra and Weld together, but can also use Mojarra with OWB, or MyFaces with Weld. However we wanted to somewhat limit the options for homegrown stacks, and a little research ahead hinted these were the more popular combinations. In a follow up poll we may zoom into this and specifically address homegrown stacks by asking which individual components people use.

An interesting observation is that the entire top 4 consists solely out of open source servers, together good for 103% relative to the amount of people who voted (remember that 1 person could vote for multiple servers), or a total of 79% relative to all servers voted for.

While these are certainly impressive numbers, we do have to realize that the voters are self selected and specifically concern those who use OmniFaces. OmniFaces is an open source library without any form of commercial support. It's perhaps not entirely unreasonable to surmise that environments that favor closed source commercially supported servers are less likely to use OmniFaces. Taking that into account, the numbers thus don't necessarily mean that open source servers are indeed used that much in general.

That said, the two big commercial servers WebSphere and WebLogic still got a fair amount of votes; 104 together which is 9% relative to all servers voted for.

The fully open source and once much talked about server Geronimo got significantly few votes; only 13. The fact that Geronimo has more or less stopped developing its server and the lack of a visible community (people blogging about it, writing articles, responding to issues etc) probably contributes to that.

It's somewhat surprising that IBM's new lightweight AS Liberty got only 9 votes, where older (and more heavier) AS WebSphere got 55 votes. Maybe Liberty indeed isn't used that much yet, or maybe the name recognition isn't that big at the moment. A potential weakness in the poll is that we left out the company names. For well known servers such as JBoss and GlassFish you rarely see people calling it Red Hat JBoss or Oracle GlassFish, but in case of Liberty it might have been clearer to call it "IBM Liberty (WLP)".

Another small surprise is that the somewhat obscure server JEUS got as many votes as it did; 11 in total. This is perhaps extra surprising since creator TMaxSoft for some unknown reason consistently calls it a WAS instead of an AS, and the poll asked for the latter.

The "Japanese obscure three" (WebOTX, Interstage AS and (u)Cosminexus) are at the bottom of the list, yet at least 3 to 4 persons each claim to be using it with OmniFaces. Since not all of these servers are trivial to obtain, we've never tested OmniFaces on any of them so frankly have no idea how well OmniFaces runs on them. Even though according to this poll it concerns just a small amount of people, we're now quite eager to try out a few of these servers in the future, just to see how things work there.

Conclusion

For the particular community of those who use Omnifaces, we've seen that open source servers in general and particularly JBoss, GlassFish and TomEE are the most popular Java EE servers. Tomcat and Jetty were included as well, but aren't officially Java EE (although one can build stacks on them that get close).

A couple of servers, which really are complete Java EE implementations just as well and one might think take just as much work to build and maintain, only see a very low amount of users according to this poll. That's of course not to say that they aren't used much in general, but may just gather to a different audience.

Arjan Tijms

Thursday, January 22, 2015

The most popular upcoming Java EE 8 technologies according to ZEEF users

I maintain a page on zeef.com about the upcoming Java EE 8 specification. On this page I collect all interesting links about the various sub-specs that will be updated or newly introduced in EE 8. The page is up since April last year and therefor currently has almost 10 months worth of data (at the moment 8.7k views, 5k clicks).

While there still aren't any discussions and thus links available for quite a couple of specs, it does give us some early insight in what's popular. At the moment the ranking is as follows:

Position Link Category
1 Java EE 8 roadmap [png] Java EE 8 overal
2 JSF MVC discussion JSF 2.3
3 Let's get started on JSF 2.3 JSF 2.3
4 Servlet 4.0 Servlet 4.0
5 Java EE 8 Takes Off! Java EE 8 overal
6 An MVC action-based framework in Java EE 8 MVC 1.0
7 JSF and MVC 1.0, a comparison in code MVC 1.0
8 Let's get started on Servlet 4.0 Servlet 4.0
9 JavaOne Replay: 'Java EE 8 Overview' by Linda DeMichiel Java EE 8 overal
10 A CDI 2 Wish List CDI 2.0

If we look at the single highest ranking link for each spec, we'll get to the following global ranking:

  1. Java EE 8 overal
  2. JSF 2.3
  3. Servlet 4.0
  4. MVC 1.0
  5. CDI 2.0
  6. JAX-RS 2.1
  7. JSON-B 1.0
  8. JCache 1.0
  9. JMS 2.1
  10. Java (EE) Configuration
  11. Java EE Security API 1.0
  12. JCA.next
  13. Java EE Management API 2.0
  14. JSON-P 1.1
Interestingly, when we don't look at the single highest clicked link per spec, but aggregate the clicks for all top links, we get a somewhat different ranking as shown below (the relative positions compared to the first ranking are shown behind each spec):

  1. Java EE 8 overal (=)
  2. MVC 1.0 (+2)
  3. JSF 2.3 (-1)
  4. CDI 2.0 (+1)
  5. Servlet 4.0 (-2)
  6. JCache 1.0 (+2)
  7. Java (EE) Configuration (+3)
  8. JAX-RS 2.1 (-2)
  9. JMS 2.1 (=)
  10. JSON-B 1.0 (-3)
  11. Java EE Security API 1.0 (=)
  12. JCA.next (=)
  13. Java EE Management API 2.0 (=)
  14. JSON-P 1.1 (=)

As we can see the specs that occupy the top 5 are still the same, but whereas JSF 2.3 was the most popular sub-spec where it concerned a single link, looking at all links together it's now MVC 1.0. The umbrella spec Java EE however is still firmly on top. The bottom segment is even exactly the same, but for most of them very few information is available so a block is basically the same as a link. Specifically for the Java EE Management API and JSON-P 1.1 there's no information at all available beyond a single announcement that the initial JSR was posted.

While the above ranking does give us some data points, we have to take into account that it's not just about the technologies themselves but also about a number of other factors. E.g. the position on the page does influence clicks. The Java EE 8 block is on the top left of the page and will be seen first by most visitors. Then again, CDI 2.0 is at a pretty good position at the top middle of the page, but got relatively few clicks. JSF 2.3 and especially MVC 1.0 are at a less ideal position at the middle left of the page, below the so-called "fold" of many screens (meaning, you have to scroll to see it). Yet, both of them received the most clicks after the umbrella spec.

The observant reader may notice that some key Java EE technologies such as JPA, EJB, Bean Validation and Expression Language are missing. It's likely that these specs will either not be updated at all for Java EE 8, or will only receive a very small update (called a MR or Maintenance Release in the JCP process).

Oracle has indicated on multiple occasions that this is almost entirely due to resource issues. Apparently there just aren't enough resources available to be able to update all specs. Even though there are e.g. dozens of JPA JIRA issues filed and persistence is arguably one of the most important aspect of the majority of (web) applications, it's just not possible to have a major update for it, unfortunately.

Conclusion

In general we can say that for this particular data point the web technologies gather the most interest, while the back end/business and supporting technologies are a little less popular. It will be interesting to see if and if so how the numbers will change when more information become available. Java EE Management API 2.0 for one seems really unpopular now, but there simply isn't much to measure yet.

Arjan Tijms

Tuesday, January 6, 2015

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