Monday, August 29, 2016

Custom authorization rules on IBM Liberty

Last month we presented a way how a Java EE application can provide custom rules for authorization. The code shown in that article was developed and tested using Payara. We're now going to look at how the code can be used on some other servers, starting with IBM's Liberty.

Liberty has a highly modularised architecture and features a rather amazing way by which and end user can compose the runtime. By means of its server.xml file, a Liberty user can add or remove nearly every individual feature that Liberty has independently. The Liberty core system keeps track of the dependencies of each module representing such feature and adds or removes its dependencies accordingly (in a way not quite unlike what a tool like Maven does at build time).

All this power does come at some price. For Liberty the JACC provider which is needed to implement the logic behind the custom authorization rules has to be turned into a Liberty specific module (called a user feature), which is quite a bit more work than needed for other servers (even the ones that are also modular).

Summarised we have to:

  • Create an OSGi bundle
  • Add our JACC provider jar to this bundle
  • Implement a Liberty specific service that returns the policy and factory instances from our provider
  • Make sure we have the right imports and exports
  • Create a Liberty Feature that references the previously created bundle and provides the right exports
  • Installing the feature in Liberty

IBM provides documentation on how to install a JACC provider within Liberty, but unfortunately the documentation is not entirely clear, and even less fortunately it's not fully correct.

As in some of the other documents from IBM the JACC installation has some very terse instructions. They may be clear if you're a rather experienced Liberty and/or OSGi user, but if you're not a sentence like the following might be a bit puzzling:

Package the component into an OSGi bundle that is part of your user feature, along with your JACC provider.
The above is more a summary of what we have to do, not how to actually do it. We look at the how in some more detail below:

Create an OSGi bundle

Luckily IBM also has instructions for a "hello world" user feature, which are much more complete and easier to follow. Not so lucky was that after creating an "OSGI Bundle Project" as indicated by those instructions I got a compile error right away in the project. For some unfathomable reason the package org.osgi.framework could not be found. Eventually it appeared that next to a target runtime (which was set to the installed Liberty server), Eclipse also has the notion of a target platform. The first one normally provides the packages a project compiles against, but in an OSGi project things are never that simple. The target platform changes automatically when you set the target runtime for your project, even though for former is global for your entire workspace and the latter is per project.

The target platform can be set in Eclipse in Preferences -> Plug-in Development -> Target Platform. See the screenshot below:

Switching to "Running platform" solved the compile error for org.osgi.framework, but the IBM specific bundles couldn't be resolved then. Switching to "Liberty beta 2016.8 with SPI" solved the problem, but later on I could switch back to "Liberty beta 2016.8" without the compile error coming back. Eclipse sometimes works in mysterious ways.

Add our JACC provider jar to this bundle

Not mentioned anywhere in the IBM documentation, but the existing JACC provider jar had to be copied into the BundleContent folder of the OSGi bundle project. The Eclipse manifest editor (runtime tab -> classpath pane) for some reason doesn't let you pick from this location, but it can be easily added directly to the MANIFEST.FM file by adding the following to it:

Bundle-ClassPath: .,
 cdi-jacc-provider-0.1-SNAPSHOT.jar
After the JACC provider jar has been added in this way, the manifest editor shows the jar, but it will not have been added to the classpath of the project itself. This has to be done separately by going to project -> properties -> Java Build Path -> Libraries and adding the jar there too. See the screenshot below:

Implement a Liberty specific service that returns the policy and factory instances from our provider

Contrary to all other servers, Liberty requires an amount of Liberty specific code in the module. The above mentioned instructions give an example of this, but unfortunately that code for a required service seemed to be rather faulty. Among others several imports were missing, the "getPolicyConfigurationFactory" method name was wrong (should be "getPolicyConfigFactory"), the return type of that same method was wrong (instead of "Policy" it should be "PolicyConfigurationFactory", then there's an active method that used a variable called "configprops" that's not defined anywhere, etc.

Additionally, the service uses the @Component annotation, which is said to generally make things easier, but I wasn't able to use it. After importing the right package for it, Liberty kept logging the following error when starting up:

[ERROR ] CWWKE0702E: Could not resolve module: JaccBundle [250] Unresolved requirement: Import-Package: org.osgi.service.component.annotations; version="1.3.0"

Being unable to resolve this, I removed the annotation and implemented a traditional Activator instead. The code for both the service and the activator is shown below:

Service:

package jaccbundle;

import static java.lang.System.setProperty;
import static java.lang.Thread.currentThread;

import java.security.Policy;

import javax.security.jacc.PolicyConfigurationFactory;

import org.omnifaces.jaccprovider.jacc.policy.DefaultPolicy;

import com.ibm.wsspi.security.authorization.jacc.ProviderService;

public class MyJaccProviderService implements ProviderService {
    
    @Override
    public Policy getPolicy() {
        return new DefaultPolicy();
    }

    @Override
    public PolicyConfigurationFactory getPolicyConfigFactory() {
        
        ClassLoader originalContextClassloader = 
            currentThread().getContextClassLoader();
        try {
            currentThread()
              .setContextClassLoader(
                      this.getClass().getClassLoader());
            
            setProperty(
                "javax.security.jacc.PolicyConfigurationFactory.provider",
                "org.omnifaces.jaccprovider.jacc.configuration.TestPolicyConfigurationFactory");
            
            return PolicyConfigurationFactory.getPolicyConfigurationFactory();
        } catch (Exception e) {
            return null;
        } finally {
            Thread.currentThread()
                  .setContextClassLoader(
                      originalContextClassloader);
        }
    }
}

Activator

package jaccbundle;

import static org.osgi.framework.Constants.SERVICE_PID;

import java.util.Hashtable;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;

import com.ibm.wsspi.security.authorization.jacc.ProviderService;

public class Activator implements BundleActivator {
    
    private static final String CONFIG_PID = "jaccBundle";
    private ServiceRegistration<ProviderService> jaccProviderService;

    public void start(BundleContext context) throws Exception {
        jaccProviderService = context.registerService(
            ProviderService.class, 
            new MyJaccProviderService(), 
            getDefaults());
    }

    public void stop(BundleContext context) throws Exception {
        if (jaccProviderService != null) {
            jaccProviderService.unregister();
            jaccProviderService = null;
        }
    }

    Hashtable<String, ?> getDefaults() {
        Hashtable<String, String> defaults = new Hashtable<>();
        defaults.put(SERVICE_PID, CONFIG_PID);
        return defaults;
    }

}

Make sure we have the right imports and exports

The Eclipse manifest editor offers a handy "Analyze code and add dependencies to the MANIFEST.FM" feature, but like the matter duplicator it doesn't work and has never worked. They can be added manually though. In the editor it's Dependencies -> Imported packages for the imports, and Runtime -> Exported packages for the exports. See below:

After some trial and error the MANIFEST.FM eventually looked like this:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: JaccBundle
Bundle-SymbolicName: JaccBundle
Bundle-Version: 1.0.0.qualifier
Bundle-Activator: jaccbundle.Activator
Bundle-ClassPath: .,
 cdi-jacc-provider-0.1-SNAPSHOT.jar
Import-Package: com.ibm.wsspi.security.authorization.jacc;version="1.0.0",
 javax.enterprise.context;version="1.1.0",
 javax.enterprise.context.spi;version="1.1.0",
 javax.enterprise.inject;version="1.1.0",
 javax.enterprise.inject.spi;version="1.1.0",
 javax.inject;version="1.0.0",
 javax.naming,
 javax.security.auth,
 javax.security.jacc;version="1.5.0",
 org.osgi.framework
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Export-Package: jaccbundle,
 org.omnifaces.jaccprovider.cdi,
 org.omnifaces.jaccprovider.jacc

Create a Liberty Feature that references the previously created bundle and provides the right exports

After having created the Liberty Feature project using the IBM instruction for the hello world feature, there's relatively little to be done here. We do have to add the exports that we already exported from the bundle again here.

This can be done via the editor for the one and only file in our feature project called SUBSYSTEM.FM:

The raw file will then look as follows:
Subsystem-ManifestVersion: 1.0
IBM-Feature-Version: 2
IBM-ShortName: JaccFeature
Subsystem-SymbolicName: JaccFeature;visibility:=public
Subsystem-Version: 1.0.0.qualifier
Subsystem-Type: osgi.subsystem.feature
Subsystem-Content: JaccBundle;version="1.0.0"
Manifest-Version: 1.0
IBM-API-Package: org.omnifaces.jaccprovider.cdi,
 org.omnifaces.jaccprovider.jacc
The IBM JACC installation instructions mentioned one other point here:
Ensure that your feature includes the OSGi subsystem content: com.ibm.ws.javaee.jacc.1.5; location:="dev/api/spec/".
I could however not get this to work. Adding it to SUBSYSTEM.FM as follows:
Subsystem-Content: JaccBundle;version="1.0.0",com.ibm.ws.javaee.jacc.1.5;location:="dev/api/spec/"
Resulted in the following error when starting Liberty:
Bundle location:="dev/api/spec/" cannot be 
 resolved
Adding it as:
Subsystem-Content: JaccBundle;version="1.0.0",com.ibm.ws.javaee.jacc.1.5
Yielded the following error:
[ERROR   ] CWWKE0702E: Could not resolve module: com.ibm.ws.javaee.jacc.1.5 [251]
  Another singleton bundle selected: osgi.identity; osgi.identity="com.ibm.ws.javaee.jacc.1.5"; type="osgi.bundle"; version:Version="1.0.13.20160722-1732"; singleton:="true"
Without this directive the JACC provider already worked as expected, so I just left it out. It's intended use is somewhat puzzling though.

The workspace with all artefacts combined for the two required projects looks as follows:

Installing the feature in Liberty

As explained in the hello world instructions by IBM, the feature with the embedded bundle with the embedded JACC provider jar can be installed via the Feature project by right clicking on it in Eclipse and selecting "Install Feature". After that is done, the final step is to add the new feature to Liberty's server.xml file, for which there again is a graphical editor in Eclipse:

The raw file look as follows:

<server description="Liberty beta">

    <featureManager>
        <feature>javaee-7.0</feature>
        <feature>localConnector-1.0</feature>
        <feature>usr:JaccFeature</feature>
    </featureManager>

    <httpEndpoint httpPort="8080" httpsPort="9443" id="defaultHttpEndpoint"/>
    
    <webContainer deferServletLoad="false"/>
    <ejbContainer startEJBsAtAppStart="true"/>

    <applicationManager autoExpand="true"/>
    <applicationMonitor updateTrigger="mbean"/>

</server>

After this we can finally run a web application to test things, and it indeed seems to work as expected!

One thing to keep in mind is that Liberty initialises the CDI request scope rather late (after authentication has been done). For some of the calls to our custom JACC provider this is way too late. Fortunately, CDI *does* work, but only for scopes that don't depend on the current request (like the application scope).

The authorization mechanism as demonstrated in the previous article most naturally uses the application scope anyway, so here that's no problem. For the brave, the request scope for CDI can forcefully and in a hacky way be activated earlier (e.g. in a JASPIC SAM) as follows:

Object weldInitialListener = request.getServletContext().getAttribute("org.jboss.weld.servlet.WeldInitialListener");
ServletRequestEvent event = new ServletRequestEvent(request.getServletContext(), request);
        
ELProcessor elProcessor = new ELProcessor();
elProcessor.defineBean("weldInitialListener", weldInitialListener);
elProcessor.defineBean("event", event);
elProcessor.eval("weldInitialListener.requestInitialized(event)");
Note again that this is a hack, and as such not guaranteed to work without any problems or keep working on newer versions of Liberty. It was only briefly tested on Liberty Beta 2016.8

Conclusion

Installing a JACC provider on Liberty is a lot of work. The vendor documentation is not clear on this and even contains errors. After a user feature has been created it's relatively easy though to add it to Liberty. Publishing it to a repository (not discussed in the article) would make it even easier to use the JACC provider on other instances of Liberty.

The amount of work that's required and the need to dive into vendor specific documentation could largely be eliminated by having a Java EE standard way to add a JACC provider from within an application (like what is currently possible for JASPIC SAMs). Hopefully a JACC MR will consider this.

Regardless, with the JACC provider installed once Liberty users can from then on easily add custom authorization rules to their applications. Care should be taken though that the JACC provider is just a POC. It shows great promise, but has not been extensively tested yet.

Arjan Tijms