Thursday, April 16, 2009

Building a Custom Trust Association Interceptor for WebSphere Portal, Part II

In Part I, we looked at the code for building a simple SSO Trust Association Interceptor for WebSphere Portal. This part explains the general steps on how to install the TAI on the application server.

The following steps describe how to Install a custom Trust Association Interceptor for WebSphere.

1.) Develop a class that extends TrustAssociationInterceptor
  • Fully qualified class for WebSphere is com.ibm.wsspi.security.tai.TrustAssociationInterceptor
  • Need to override the following methods:
  • initialize: initializes the TAI
  • isTargetInterceptor: determines if this TAI should be used as the one to check for Trust Association for the requested resource
  • negotiateValidateandEstablishTrust: this method does the actual checking to see if we can create the custom TAI Subject that is passed to the underlying protected resource
2.) Add the following libraries to the build path - (for development purposes. Project will have compilation errors without them)
  • sas.jar
  • wssec.jar
3.) Export the jar
  • Needs to be installed in the /lib/ext directory on all nodes for the application server
  • Any other necessary jar files used by the TAI should be placed in the /lib directory
4.) Configure the TAI on the ND
  • Security > Authentication Mechanisms > LPTA > Trust Association > Interceptors
  • Choose to create a new Interceptor
  • Enter the fully qualified class name of the Interceptor class (package + class name)
  • Apply, then Ok
6.) Enable Security on both WebSphere Application server and WebSphere Portal (if this hasn't been done yet). See here for more information.

5.) Restart all nodes

You should be able to see a print out at Server start up indicating it has loaded the new TAI.

Building a Custom Trust Association Interceptor for WebSphere Portal

At one point, I had to develop a custom Single Sign On solution to WebSphere Portal. The general standard for this is to use TAM, but since this wasn't available, I wrote something a little more simple, using a Trust Association Interceptor. Trust Association Interceptors are used when a page in a Web Application has been marked as protected. The TAI can determine if a user should be allowed to access that page.

This multiple part posting explores setting up a simple TAI for WebSphere Portal. It's a collection of information around the web. For an excellent resource on Trust Association Interceptors, see this article (which I wish I had seen when I did this).

As usual, the code has been stripped down to the key parts.

Part I: The code

package test.security.tai;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.ibm.websphere.security.CustomRegistryException;
import com.ibm.websphere.security.EntryNotFoundException;
import com.ibm.websphere.security.UserRegistry;
import com.ibm.websphere.security.WebTrustAssociationException;
import com.ibm.websphere.security.WebTrustAssociationFailedException;
import com.ibm.wsspi.security.tai.TAIResult;
import com.ibm.wsspi.security.tai.TrustAssociationInterceptor;

/**
* Custom Login Module
*
* Project imports the jar wssec.jar for development purposes.
* Found in the server runtime lib directory ($irad_home$\runtimes\base_v6\)
*
*
**/
public class CustomPortalTAI implements TrustAssociationInterceptor
{
private static final String VERSION = "Custom TAI version 1.0 \n Author: SirCrofty \n " + "Last Updated: March 1, 2008";

private static final String TYPE = "--- Custom TAI --- \n Custom Trust Assocation Interceptor for WebSphere Portal Application";

HashMap sharedState = null;

/**
* Constructor
*
**/
public CustomPortalTAI()
{
sharedState = new HashMap();
}

/**
* (non-Javadoc)
* @see com.ibm.wsspi.security.tai.TrustAssociationInterceptor#initialize(java.util.Properties)
* @param arg0
* @return
* @throws com.ibm.websphere.security.WebTrustAssociationFailedException
*
**/
public int initialize(Properties props) throws WebTrustAssociationFailedException
{
return 0;
}


/**
* (non-Javadoc)
* @see com.ibm.wsspi.security.tai.TrustAssociationInterceptor#isTargetInterceptor(javax.servlet.http.HttpServletRequest)
* @param arg0
* @return
* @throws com.ibm.websphere.security.WebTrustAssociationException
*
**/
public boolean isTargetInterceptor(HttpServletRequest req) throws WebTrustAssociationException
{
System.out.println("*********** Custom TAI ******************");
System.out.println("Determining if this TAI should handle the incoming request...");

if (req.getParameter("customUser") != null)
{
System.out.println("Custom TAI is being used to establish trust!");
return true;
}


System.out.println("Bypassing Custom TAI, did not find a user ID in the request");
return false;
}
/**
* (non-Javadoc)
* @see com.ibm.wsspi.security.tai.TrustAssociationInterceptor#negotiateValidateandEstablishTrust(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
* @param arg0
* @param arg1
* @return
* @throws com.ibm.websphere.security.WebTrustAssociationFailedException
*
**/
public TAIResult negotiateValidateandEstablishTrust(HttpServletRequest req, HttpServletResponse resp)
throws WebTrustAssociationFailedException
{
String userId = req.getParameter("customUser");
if (userId.equals("portalUser"))
{
System.out.println("*********** CustomTAI *****************");
System.out.println("UserID = " + userId);

return TAIResult.create(HttpServletResponse.SC_OK, userId);
}
else
{
return TAIResult.create(HttpServletResponse.SC_FORBIDDEN, userId);
}
}

/**
* @see com.ibm.wsspi.security.tai.TrustAssociationInterceptor#cleanup()
*
*
**/
public void cleanup()
{
sharedState = null;
}


/**
* @see com.ibm.wsspi.security.tai.TrustAssociationInterceptor#getType()
* @return
*
**/
public String getType()
{
return TYPE + " \n " + this.getClass().getName();
}

/**
*
* @see com.ibm.wsspi.security.tai.TrustAssociationInterceptor#getVersion()
* @return
*
**/
public String getVersion()
{
return VERSION;
}
}

We're implementing the com.ibm.wsspi.security.tai.TrustAssociationInterceptor interface provided by IBM. In order to get this to compile, you may need to place some jars in your class path during development. These jars are named wssec.jar and sas.jar, and are found in your server runtime directory.

The two methods of interest above are isTargetInterceptor and negotiateValidateandEstablishTrust. Both of these methods accept an HttpServletRequest as input, and that's how we can accomplish our single sign on.

isTargetInterceptor is called whenever a user requests access to a protected page. The Application server will run through it's list of installed TAIs (see Part II for installing the TAI) and call isTargetInterceptor on each one. This is the servers way of determining if it should use that TAI for the incoming request. In our simple example, if the request has the parameter customUser, we tell the application server to return true, signaling that we want to use this TAI.

Once the server finds a TAI that returns true for it's isTargetInterceptor method, it will proceed to call that TAI's negotiateValidateandEstablishTrust method. This method is in charge of actually checking whether we want to trust the incoming request, and therefore forward the user to the requested page.

In the example, if the userId is equal to "portalUser", we create a TAIResult with a 200 response, indicating that all is good and the user can continue. Otherwise, the access is forbidden.

Since the url path /myportal is protected once security is configured on WebSphere Portal, all requests sent to /myportal will be challenged against this TAI. If the request included a query parameter such as /myportal?customUser=portalUser, the CustomPortalTAI would be invoked, and the user would be passed to the corresponding page in the portal.

Note that we're not creating any Subject information or anything else to create the user credentials. WebSphere Portal will take care of most of the default creation for you, once the success is found in the TAI.

Monday, April 13, 2009

Funny article on programming progression

Found this online, and although it's a little older, I thought it was pretty funny.

Evolution of a Programmer

Thursday, April 9, 2009

Fixing WPSconfig if cannot find native2ascii

This really only applies to older versions of WebSphere Portal (pre 6.1, as I believe it uses ConfigEngine now), but I've encountered this a couple times for a variety of reasons:

Executing native2ascii with native encoding 'UTF-8': wpconfig.properties -> wpconfig_ascii.properties

java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:85)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:58)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:60)
at java.lang.reflect.Method.invoke(Method.java:391)
at com.ibm.ws.bootstrap.WSLauncher.run(WSLauncher.java:222)
at java.lang.Thread.run(Thread.java:570)
Caused by: java.lang.NoClassDefFoundError: sun/tools/native2ascii/Main
at com.ibm.wps.config.WpsConfigMain.convertToAscii(WpsConfigMain.java:991)
at com.ibm.wps.config.WpsConfigMain.process(WpsConfigMain.java:523)
at com.ibm.wps.config.WpsConfigMain.main(WpsConfigMain.java:204)
... 7 more


Just add the following to the classpath portion of the command at the bottom of the WPSconfig.sh or WPSconfig.bat file:

$JAVA_HOME/lib/tools.jar

The jar tools.jar is missing from the classpath apparently, or buried too far down, and the script is dependent on this to create the OS specific properties file to run the task.

Monday, April 6, 2009

Google Geocoding and Restlet

The other week I needed to code a web page containing interaction between a set of elements and Google Maps, driven off a list of html links in a separate section of the page . The elements had latitude and longitude fields in the database, but this information wasn't completely filled in, especially for internationally based elements.

Given that I needed to be able to click on the links and bring back an individual element (or a subset of elements), I pulled the data out of the database and shoved it in a cache, rather than looking up the data each time (basic stuff).

The problem was, I had about a quarter of the elements lacking the necessary coordinate information to plot on the map.

So the idea was to fill in as much as the missing information at cache load, using Google's Gecoding service. To call the Google Gecoding API, I used the Restlet framework. Code is below, note that not all classes are included (such as model objects and constants files). The cache implementation is Ehcache.

All code stripped down to protect the innocent.

Method to load cache:

public synchronized void load()
{
PropertyDAO propDao = new PropertyDAOImpl(conn);
List<Property> propList = null;

try
{
// get the properties from growth right now
propList = propDao.getPropertiesGeoInfo();
}
catch (SQLException e)
{
e.printStackTrace();
}

if (propList != null)
{
// get the property cache
CacheManager manager = CacheManager.getInstance();
Cache propCache = manager.getCache("myCache");

// for each property, check if it has latitude and longitude, fill in if it doesn't and put in the list
for (Property prop : propList)
{
// check to see if there is a latitude and longitude
if (prop.getLatitudeNbr() == 0 || prop.getLongitudeNbr() == 0)
{
lookupGeoInfo(prop);
}

// add it to the cache
propCache.put(new Element(prop.getFacilityNbr(), prop));
}
}
}
The Property object is the pojo representing the element to be mapped. It comes from the data source with latitude and longitude information, but if it's empty, that's where the Geocoding service comes in:

private void lookupGeoInfo(Property prop)
{
// create a new restlet request
Request request = new Request();
Reference ref = new Reference("http://maps.google.com/maps/geo");

// map the parameters to the geocoding service
mapParams(ref, prop);

request.setResourceRef(ref);
request.setMethod(Method.GET);

Client client = new Client(Protocol.HTTP);

// call Google and get the response back
Response response = client.handle(request);

if (response != null)
{
Representation rep = response.getEntity();

try
{
/***
* split the tokens of the response.
* The format is 4 tokens:
* 200,6,42.7,-73.69
* Status code, accuracy, latitude, longitude
*
**/
String[] tokens = rep.getText().split(",");
// if the response code is success, get the lat and long and set it on the prop object
if ("200".equalsIgnoreCase(tokens[0]))
{
prop.setLatitudeNbr(Double.parseDouble(tokens[2]));
prop.setLongitudeNbr(Double.parseDouble(tokens[3]));
}
}
catch (IOException ie)
{
ie.printStackTrace();
}
}

// explicitly close the call
request.release();
}
Here we create a Restlet Request and set it's endpoint to Google's Geocoding endpoint. Once we've established where the request is going, we set the parameters Google expects (handled by mapParams). Note the format of the response - in this case, I just want the latitude and longitude, so I request the response to come back as csv format and parse it according to it's positions. There are other formats available, check the Geocoding Responses section for more info.

mapParams method:

private void mapParams(Reference ref, Property prop)
{
// flag to know whether to add a comma or not
boolean first = true;

StringBuilder sb = new StringBuilder();
// add address line one if it's there
if (!nullOrEmpty(prop.getAddrLn1Txt()))
first = formatForParam(prop.getAddrLn1Txt(), first, sb);

if (!nullOrEmpty(prop.getAddrLn2Txt()))
first = formatForParam(prop.getAddrLn2Txt(), first, sb);

if (!nullOrEmpty(prop.getCityName()))
formatForParam(prop.getCityName(), first, sb);

if (!nullOrEmpty(prop.getStateCd()))
formatForParam(prop.getStateCd(), first, sb);

if (!nullOrEmpty(prop.getPostalCd()))
formatForParam(prop.getPostalCd(), first, sb);

if (!nullOrEmpty(prop.getCountryName()))
formatForParam(prop.getCountryName(), first, sb);

// now the address parameter built from the prop object
ref.addQueryParameter("q", sb.toString());

// add the sensor param (true for a gps device)
ref.addQueryParameter("sensor", "false");

// the desired output - all we need is the csv, we don't care about other data they send back
ref.addQueryParameter("output", "csv");

// the encoding the response is coming back in
ref.addQueryParameter("oe", "utf8");

// add the key to geo code request
ref.addQueryParameter("key", geoCodeKey);
}
The Reference.addQueryParameter method adds a query string parameter to your outgoing Restlet Request, allowing you to set to set parameters more elegantly than building the whole url in a StringBuilder/StringBuffer. The method attempts to build the address line off of the Property object fields, and then sets all other parameters for the geocoding service appropriately. Note that you'll need a key from Google to perform this call, same as you would with Google Maps.

Putting it all together:

public class PropertyCacheLoad
{
private Connection conn = null;
private String geoCodeKey = null;

public PropertyCacheLoad(Connection conn, String geoCodeKey)
{
this.conn = conn;
this.geoCodeKey = geoCodeKey;
}

/*
*
*/
public void configure(String configFile)
{
CacheManager.create(configFile);
}

/*
*
*/
public synchronized void load()
{
PropertyDAO propDao = new PropertyDAOImpl(conn);

List<property:gt; propList = null;

try
{
// get the properties from growth right now
propList = propDao.getPropertiesGeoInfo();
}
catch (SQLException e)
{
e.printStackTrace();
}

if (propList != null)
{
// get the property cache
CacheManager manager = CacheManager.getInstance();
Cache propCache = manager.getCache("myCache");

// for each property, check if it has latitude and longitude, fill in if it doesn't and put in the list
for (Property prop : propList)
{
// check to see if there is a latitude and longitude
if (prop.getLatitudeNbr() == 0 || prop.getLongitudeNbr() == 0)
{
lookupGeoInfo(prop);
}

// add it to the cache
propCache.put(new Element(prop.getFacilityNbr(), prop));
}
}

}

/**
* @param prop
*/
private void lookupGeoInfo(Property prop)
{
// create a new restlet request
Request request = new Request();
Reference ref = new Reference("http://maps.google.com/maps/geo");

// map the parameters to the geocoding service
mapParams(ref, prop);

request.setResourceRef(ref);
request.setMethod(Method.GET);

Client client = new Client(Protocol.HTTP);

// call Google and get the response back
Response response = client.handle(request);

if (response != null)
{
Representation rep = response.getEntity();

try
{
/***
* split the tokens of the response.
* The format is 4 tokens:
* 200,6,42.7,-73.69
* Status code, accuracy, latitude, longitude
*
**/
String[] tokens = rep.getText().split(",");
// if the response code is success, get the lat and long and set it on the prop object
if ("200".equalsIgnoreCase(tokens[0]))
{
prop.setLatitudeNbr(Double.parseDouble(tokens[2]));
prop.setLongitudeNbr(Double.parseDouble(tokens[3]));
}
}
catch (IOException ie)
{
ie.printStackTrace();
}
}

// explicitly close the call
request.release();
}

/**
* @param ref
* @param prop
*/
private void mapParams(Reference ref, Property prop)
{
// flag to know whether to add a comma or not
boolean first = true;

StringBuilder sb = new StringBuilder();
if (!nullOrEmpty(prop.getAddrLn1Txt()))
first = formatForParam(prop.getAddrLn1Txt(), first, sb);

if (!nullOrEmpty(prop.getAddrLn2Txt()))
first = formatForParam(prop.getAddrLn2Txt(), first, sb);

if (!nullOrEmpty(prop.getCityName()))
formatForParam(prop.getCityName(), first, sb);

if (!nullOrEmpty(prop.getStateCd()))
formatForParam(prop.getStateCd(), first, sb);

if (!nullOrEmpty(prop.getPostalCd()))
formatForParam(prop.getPostalCd(), first, sb);

if (!nullOrEmpty(prop.getCountryName()))
formatForParam(prop.getCountryName(), first, sb);

// now the address parameter built from the prop object
ref.addQueryParameter("q", sb.toString());

// add the sensor param (true for a gps device)
ref.addQueryParameter("sensor", "false");

// the desired output - all we need is the csv, we don't care about other data they send back
ref.addQueryParameter("output", "csv");

// the encoding the response is coming back in
ref.addQueryParameter("oe", "utf8");

// add the key to geo code request
ref.addQueryParameter("key", geoCodeKey);
}

/**
* @param s
* @return
*/
private boolean nullOrEmpty(String s)
{
return (s == null || s.trim().length() == 0);
}

/**
* @param s
* @param first
* @param sb
* @return
*/
private boolean formatForParam(String s, boolean first, StringBuilder sb)
{
if (first)
sb.append(s);
else
sb.append(", " + s);

return false;
}

}