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

Comments

Popular posts from this blog

Implementing container authentication in Java EE with JASPIC

Jakarta EE Survey 2022

Counting the rows returned from a JPA query