Friday, May 15, 2015

Reading Values from Custom OpenAM Admin Console Pages

Back in December I outlined how to add configuration pages into OpenAM's admin console and promised to show how to read those values out. This post fulfills that promise.

The first thing to be aware of before reading content from OpenAM's configuration is that if you access that configuration before OpenAM's infrastructure has started up completely, OpenAM essentially becomes brain dead and won't respond until the container is restarted. See my previous post on that topic to know how to avoid that problem. With that out of the way lets dive into listeners.

Listening for Changes


Since your admin console page can be accessed at any point in time, values could be changing that impact how your code should function. For example, in that previous post on adding console pages I used Radius configuration as the requirements for my page. One of the properties indicates if the radius server should be enabled or not. If it is not running and we enable it in that page and press the Save button we would expect it to start up immediately after. Listeners are the means of doing that.

A listener need only be added once during the lifetime of Open AM's instance in its container. The radius code is enabled with a ServletContextListener. Its contextInitialized method is called once by a single thread as part of the guarantee of such listeners. So I instantiate a ConfigLoader class that has an instance variable that it uses to hold the ID of the registered listener. If that variable is empty then I am safe to register it. If you are using some other mechanism to start your services up and it is possible to have multiple threads in there at the same time, then you'll have to take extra precautions with your concurrency. Another place where concurrency is important is when receiving events as we'll see below.

To register a listener you must implement the com.sun.identity.sm.ServiceListener class located in the openam-core jar. It has the following methods:

public void schemaChanged(String serviceName, String version);
public void globalConfigChanged(String serviceName, String version, 
         String groupName, String serviceComponent, int type);
public void organizationConfigChanged(String serviceName, 
         String version, String orgName, String groupName, 
         String serviceComponent, int type);

As noted in my previous post on adding pages to the console, the radius configuration only uses global pieces (radius server specific pieces like what port to listen on) with subschema items (defined radius clients). When any of those change the only method that gets called is globalConfigChanged. I have not tested beyond that to know when the other two methods are called.

But remember that the configuration may change at any point in time including immediately after it was just called. For example, suppose that I enabled the radius server and pressed Save and then remembered that I needed to change the port as well. So I chang the port and hit Save again. I know, finicky user syndrome. But that's the way it goes sometimes. What happens if I use the thread that is calling my listener to drive the startup and shutdown of the radius server listener? That could be a problem in this case if the service that is calling me doesn't ensure sequential, non-overlapping calls. One thread could be attempting to open the port while the second is attempting to shut it down and then restart on another port. Better to be certain of what is happening and when.

So in the radius server listener's case it accepts an instance of ArrayBlockingQueue in its constructor. Then, when changes occur, it calls the queue's offer method. What you pass to that call is really irrelevant and can be whatever makes sense for your application. I'm passing a String that I then log in the receiver (not shown). And you can see that I'm handling the case where my offer is rejected due to a full queue:

public void globalConfigChanged(String serviceName, String version, String groupName, String serviceComponent, int type) {
    boolean accepted = configChangedQueue.offer("RADIUS Config Changed. Loading...");

    if (! accepted) {
        cLog.log(Level.INFO, "RADIUS Client handlerConfig changed but change queue is full. Only happens when previous " +
            "change event takes too long to load changes. Existing queued events will force loading of these changes. " +
            "Therefore, dropping event.");
    }
}

What is important is that you can separately have a daemon thread waiting for items to be put into the queue, handling updates in configuration, adjusting the server as needed when an item shows up, and then go back waiting for the next item to appear only after all effects from the current change have settled.

Registering Your Listener


To register your listener you need an instance of com.sun.identity.sm.ServiceConfigManager also found in openam-core. Since ServiceConfigManager is a mouthful I'll refer to it as the  SCM in the rest of this post. In my previous post I noted that the name attribute of the Service element in your service descriptor file would be necessary to access your configured values. Well here is where that comes into play. To instantiate an instance of the SCM we pass that value to its constructor. Along with it we need an admin token and once we have the SCM instance for our service, we then register our listener via the addListener method as shown below.


SSOToken admTk = (SSOToken) AccessController.doPrivileged(
    AdminTokenAction.getInstance());
ServiceConfigManager mgr = new ServiceConfigManager(
    Constants.RADIUS_SERVICE_NAME, admTk);

if (mgr != null) {
    if (listenerId == null) {
        this.listenerId = mgr.addListener(
            new ConfigChangeListener(configChangedQueue));
    }
    ... other code as needed.
}

Loading Your Configuration


Now lets load the information. The SCM is specifically for my config page service since I handed it the name of that configuration. At this point I'll show what I was told to use by a forge rock employee but I can't explain much detail on the API  nor could he. I hope forge rock can make this clear via documentation at some point. And if you know of such documentation please leave a comment with a link to it.

Items beneath the Global element from the service descriptor file in my previous post appear in the Radius Server page such as port and whether it is enabled or not. Items in its SubSchema element are mapped to radius client instances and show in the Radius Server page's table. To get the items in the Radius Server page I do the following. No, I don't use strings directly in code like this but I put them in here for this post so that you can associate the handling here with the corresponding name attribute of the AttributeSchema elements in the service descriptor file. I've only included the handling for two of the server specific properties:

ServiceConfig serviceConf = mgr.getGlobalConfig("default");

if (serviceConf != null) {
    List<ClientConfig> definedClientConfigs = 
        new ArrayList<ClientConfig>();
    boolean isEnabled = false;
    int listenerPort = -1;
    Map<String, Set<String>> map = serviceConf.getAttributes();
    int coreThreads = -1;
    int maxThreads = -1;
    int queueSize = -1;
    int keepaliveSeconds = -1;

    for (Map.Entry<String, Set<String>> ent : map.entrySet()) {
        String key = ent.getKey();
        String value = extractValue(ent.getValue());
        if ("radiusListenerEnabled".equals(key)) {
            isEnabled = "YES".equals(value);
        }
        // don't need to catch NumberFormatException due to 
        // limiting values in the xml file enforced by console
        else if ("radiusServerPort".equals(key)) {
            listenerPort = Integer.parseInt(value);
        }
        ...handle other values/client instances (see below)
    }


The extractValue method handles an annoying issue. The object that we get for each attribute is a Set object even if there is only a single item contained there-in.  Its code looks as follows:


String extractValue(Set<String> wrappingSet) {
    String[] vals = wrappingSet.toArray(Constants.STRING_ARY);
    return vals[0];
}

To get the radius clients I do the following. On the ServiceConfig object for that "default" global configuration I call its getSubConfigNames method. The names here correspond to the name of each radius client and to the names that show in the Radius Server page's table. For each of those I then ask for its corresponding ServiceConfig object and get its defined attribute just like the handling above. Again, I've only showed handling for a couple of items:

    Set<String> names = serviceConf.getSubConfigNames();

    for (String s : names) {
        // object for holding values in memory
        ClientConfig clientConfig = new ClientConfig(); 
        clientConfig.name = s;

        // go get our admin console values
        ServiceConfig clientCfg = serviceConf.getSubConfig(s);
        map = clientCfg.getAttributes();

        // now just like above we pull out the values by field name
        for (Map.Entry<String, Set<String>> ent : map.entrySet()) {
            String key = ent.getKey();

            if ("clientIpAddress".equals(key)) {
                clientConfig.ipaddr = extractValue(ent.getValue());
            }
            ... other fields
            else if ("handlerConfig".equals(key)) {
                clientConfig.handlerConfig =
                    extractProperties(ent.getValue());
            }
        }
        definedClientConfigs.add(clientConfig);
    }

The handlerConfig attribute corresponds to the list box in the radius client configuration page in which we can enter any number of Strings. I expect it to be a name and value pair separated by an equals character. (I hope to replace this control altogether shortly with a drop down selection box to avoid user input errors but haven't had the time yet.) The  extractProperties method splits each of those into the name and value and injects them all into a Properties object.

Properties extractProperties(Set<String> wrappingSet) {
    String[] vals = wrappingSet.toArray(Constants.STRING_ARY);
    Properties cfg = new Properties();

    for(String val:vals) {
        int idx = val.indexOf('=');

        if (idx == -1) {
            cfg.setProperty(val, "");
        }
        else {
            cfg.setProperty(val.substring(0, idx), 
                val.substring(idx + 1));
        }
    }
    return cfg;
}


Conclusions And Plea

At this point I have the Radius Server page values and an array of ServiceConfig objects that are just POJOs to hold each client's value. And with that information the daemon thread then goes and make the corresponding changes to the radius server and goes back to listening.

That concludes what I've been able to figure out on how to load values managed within OpenAM's Admin Console and registering for and receiving notification of their changes. As yet I've been unable to write these values via code. If anyone has documentation on this API please leave a comment linking to those resources. It would be very beneficial to us all. I hope this has been helpful to someone.

Enjoy.


No comments:

Post a Comment