Wednesday, February 10, 2010

A simple remote API that modifies WebLogic Portal (WLP) Visitor Entitlements

Recently, I was looking for a simple remote API to modify the visitor entitlement roles available inside of a portal. Specifically, I wanted to check if a role existed, and if it didn't create it. The entire use case is that the role policies are going to be defined in Oracle Entitlements Server (OES) but the policy (assigning or roles to WLP artifacts like desktops and portlets) is going to be handled inside of WLP. We'll leave the OES pieces for another time. The focus on this post is the WLP APIs that are available for manipulating the visitor entitlements.

After reviewing what I knew about WLP Vistor entitlements and scanning the documentation, I presented two approaches to the WLP team - direct modification of the database or some use of the RolePolicyManager. I was pushed towards the API approach - after all - that's what its there for. I was initially leaning towards the database because I wanted to invoke this API simply, and you can do quite a bit with SQL Ant Task. So, off I went to try to figure out how to use the RolePolicyManager.

First of all, I'm not a WLP expert, so if this is a well know issue, then read on, but I had trouble getting Workshop to generate a valid WLP EAR. I had this problem - some issue with the facets being out of whack. Thanks gsmith.

Moving on, now that I had an EAR, the next step was to figure out how to use the RolePolicyManager to search for a role. The first thing that helped was turning on some WLP debugging. I set-up a debug file file this:

# turn on package names for debugging
usePackageNames: on


# append output to mydebug.log file rather than System.err
out.file = mydebug.log

com.bea.p13n.entitlements.management: on

This dumps out a lot of good information. So, with debug on, and clicking through the WLP Visitor Entitlements screens, I get some output like this:

[com.bea.p13n.entitlements.management.internal.RDBMSRolePolicyManager.getRolePolicy():347] get role policy:
id: type=, EntApp=MyPortalEAR, Webapp=MyWeb, Resource=com_bea_p13n_role_policy_pool, Capability=
policy name: AnonymousVisitor

This lead to my first ah-ha! I was able to return the list of Roles for the web app by making the following API call

out.println(java.util.Arrays.asList(RolePolicyManager.listRolesForResource(ear,war,"com_bea_p13n_role_policy_pool")));

com_bea_p13n_role_policy_pool is the resource. The javadoc then tells me that the String [] returned contains role policy names.

At this point, I'm almost there with all of the parameters that I need to create a RolePolicyItem and see if it exists. The only missing piece is the resourceScope. All of the likely values are available on EntitlementsConstant. After some experimenting, the answer is EntitlementConstants.ENT_APP_ROLE_INHERITANCE. I'm now able to check and see if a role policy exists.

com.bea.p13n.entitlements.policy.RolePolicyItem ri =
new com.bea.p13n.entitlements.policy.RolePolicyItem();

ri.setEntAppName(ear);
ri.setResourceId("com_bea_p13n_role_policy_pool");
ri.setWebAppName(war);
ri.setPolicyName(role);
ri.setResourceScope(com.bea.p13n.entitlements.common.EntitlementConstants.ENT_APP_ROLE_INHERITANCE);


com.bea.p13n.entitlements.policy.RolePolicyItem ri2 =
rpm.getRolePolicy(ri);

if (ri2==null) {

rpm.createRolePolicy(ri);
out.println("Role "+role+" added.");

} else {

out.println("Role "+role+" already exists");

}


If the role doesn't exist it returns null. All that's left is to create the role, and that's a line call. The next time the administrator logs into the portal administration - the role is there :)

One problem that I had to overcome. I'd been invoking this from inside an unprotected JSP. When I go to call the getRolePolicy method, I get an exception that I'm not authorized. To work around this, I move the jsp to a folder called admin and protect that folder with BASIC auth and restrict access to only the role Admin which includes the user weblogic. From web.xml:



Admin Resources
/admin/*


Admin



Admin


BASIC


and weblogic.xml


Admin




So now I have a JSP packaged in a separate EAR that can add a role to any webapp in any ear deployed on the cluster. Also, by placing the JSP in a different WAR and protecting it by BASIC, its pretty simple to go an invoke it remotely. I used cURL


I hope that this post helps other people who need a simple remote API for manipulating the WLP Visitor Entitlements. The techniques discussed (as well as actually looking at the p13 schema) should give you enough information to extend this approach to other operations and scenarios.

Monday, February 8, 2010

Extending Universal Content Management (UCM) Security with Oracle Entitlements Server (OES) - Part 2

This is a continuation of my post from last week describing an integration between OES and UCM. This post is the fun post where I get to talk about the functionality in the solution and some more of the technical details.

Overview


Basically, what I've built is a custom component inside of UCM that makes calls to the OES Java API. To be specific, the component actually talks to a wrapper which in turn uses Java refection to communicate to the actual Java API. This is a consequence of using the custom-classloader example. As I discussed previously, the benefit of doing this is that the OES component can have an isolated classloader which comes in very handy since UCM loads all of the classes that all of the custom components need into the system classloader. I think that this is not really a big deal, and most of the useful APIs have already been handled in the examples.

The OESRuntimeWrapperImpl is the only class in the oesucmclassloader.jar. This jar along with all of the other jars in the BEA_HOME/ales32-ssm/java-ssm/lib directory are loaded by the custom classloader. How are this and the OES Java API then available to the oes-ucm component (sample project) which is loaded by the system classloader? Its all handed by the OESRuntimeWrapper

customLoader = new CustomClassLoader(Thread.currentThread().getContextClassLoader(), u);


Thread.currentThread().setContextClassLoader(customLoader);

// load the entry class using custom classloader
Class myClass = Class.forName("oes.OESRuntimeWrapperImpl", true, customLoader);
Class[] paramType;
// get references to all the needed methods

paramType = new Class[] { Object.class,
String.class,
String.class,
Properties.class,
Map.class };

isAccessAllowedMethod = myClass.getMethod("isAccessAllowed", paramType);

paramType = new Class [] { String.class, Object.class};

assertIdentityMethod = myClass.getMethod("assertIdentity",paramType);

paramType = new Class[] { CallbackHandler.class };
authenticateMethod = myClass.getMethod("authenticate", paramType);

paramType = new Class[] { String.class,
String.class,
String.class,
String.class };


// call the constructor
Constructor cons = myClass.getConstructor(paramType);
Object[] args = { alesAppConfigName, alesAppConfigId, resourceAuthority, actionAuthority };
OESRuntimeWrapperImplObj = cons.newInstance(args);

When the OESRuntimeWrapper gets created, it instantiates java.lang.reflect.Method objects for each of the methods in the OESRuntimeWrapper, and then uses those same Method objects, but instead invokes them on the OESRuntimeWrapperImpl object. Below is the example of the OESRuntimeWrapper.isAccessAllowed

public Boolean isAccessAllowed(Object authenticIdentity, String runtimeResource, String runtimeAction
, Properties appContext, Map responseContext)
{
ClassLoader threadCLDR = Thread.currentThread().getContextClassLoader();

Boolean result = null;

try
{

Thread.currentThread().setContextClassLoader(customLoader);
Object paramObj[] = { authenticIdentity, runtimeResource, runtimeAction, appContext, responseContext };

result = (Boolean) isAccessAllowedMethod.invoke(OESRuntimeWrapperImplObj, paramObj);

}
catch (InvocationTargetException refEx)
{
Throwable srcEx = refEx.getCause();
if (Error.class.isInstance(srcEx)) throw (Error) srcEx;
if (RuntimeException.class.isInstance(srcEx)) throw (RuntimeException) srcEx;
throw new RuntimeException("InvocationTargetException" + srcEx.getMessage(), refEx);

}
catch (Exception e)
{
// LOGGER.error("Error getDescription ", e);
throw new RuntimeException(e.getMessage(), e);

}
finally
{
Thread.currentThread().setContextClassLoader(threadCLDR);
}

if (null != result) return result.booleanValue();
return false;

}

The reason I'm going into all of this detail is so that if you need to modify any of the methods in the sample to expose different OES APIs (like for example, to do an authorization query), that you'll know how. Remember, if you make any changes to the OESRuntimeWrapperImpl, you need to rebuild the oesucmclassloader.jar and deploy it to the BEA_HOME/ales32-ssm/java-ssm/lib directory

The oes component for UCM


Let's start by looking at the UCM Component Wizard's view of the oes component


Initialization


On the Java tab, we can see the Filters and Class Aliases that this component uses. The extraAfterConfigInit,extraBeforeCacheLoadInit,extraAfterServicesLoadInit, and initSubjects are all filters called at various phases of initialization of UCM. The oesInstallFilter is called during the extraAfterServicesLoadInit and instatiates the OESRuntimeWrapper

String configID = SecurityModule.tryGetPolicyDomainName();
runtime = OESRuntimeWrapper.getInstance("Java API Example Application", configID, "exampleResource", "exampleAction");

Object obj = runtime.assertIdentity("USERID_TOKEN","weblogic");
System.out.println(obj);

This is as good as time as any to discuss the UsernameIdentityAsserter and how its used within the oes component. Basically, this IdentityAsserter will generate an authentic identity as long as you pass it a valid username. The ultimate validity of the name falls to the configured authentication providers in the configured SM instance. In order to avoid replicating all of the usernames in UCM into the OES RDBMS authentication provider I used a trick that enables any username to be valid. The trick is in the SQL query that the authentication provider used to validate the user. Change the Entry SQL Query to Verify User to
Select ? from dual
. As you'll see in other examples inside of the component, we're retrieving the username from the UserData class, so the right solution to identity assertion problem is to replace the example UsernameIdentityAsserter with an IdentityAsserter that take the UserData object, thus avoiding the Worlds Most Dangerous IdentityAsserter.

checkExtendedSecurityModelDocAccess


For people that don't have the HowToComponentsBundle handy, this is the definition for the checkExtendedSecurityModelDocAccess Filter


During a security check for a document, executed to augment the normal security levels to grant access to other documents.
Cached Objects: Integer desiredPrivilege, intradoc.data.ResultSet securityProfileResultSet, intradoc.data.DataBinder securityProfileData, Boolean securityResult, String securityResultMsg


So, this is the Filter that gets called when a user attempts to access a single document/folder. I'm not UCM API expert, but you can check out the cleverly named DocAccess class to see how I'm able to pull the relevant pieces (user, action, resource) out of the Filter and call the OES API. In the example, I was only interested in documents, not folders, so I only make the isAccessAllowed call when I get passed an account or a security group (this seems to only occur on documents). I also based the URL of the URL of the document. You'll have to tweak the resource scheme to meet your needs. I think one important aspect of the solution is the passing of all of the meta-data on the document down to OES as attributes. There are 50+ attributes available by default and you can add custom attributes. This is the foundation for some very powerful label based or attribute based access control models.

CommonSearchConfigCompanion


By extending the intradoc.search.DBSearchConfigCompanion class, the oes.Search class can be used to add search filters based on response returned by OES policies. All of this "magic" happens in the prepareQueryText method.

@Override
public int prepareQueryText(DataBinder dataBinder,
ExecutionContext executionContext) throws DataException {
System.out.println("Before PrepareQueryText "+dataBinder.getLocalData());

String queryText = (String)dataBinder.getActiveAllowMissing("QueryText");

System.out.println("WC: QueryText="+queryText);

//queryText = "xoesroles <contains> `stateFL`";
String user = dataBinder.getLocal("dUser");

if (user.equals("sysadmin")) {
int rc = super.prepareQueryText(dataBinder, executionContext);
System.out.println("After PrepareQueryText "+dataBinder.getLocalData());
return rc;
}

String action = "search";

String resource = oesInstallFilter.getResourcePrefix();

Object authenticIdentity = oesInstallFilter.getRuntime().assertIdentity("USERID_TOKEN",user);

HashMap responses = new HashMap();

boolean isAccessAllowed = oesInstallFilter.getRuntime().isAccessAllowed(authenticIdentity,resource,action,dataBinder.getLocalData(),responses);

System.out.println(isAccessAllowed+" "+responses);

if (!isAccessAllowed) {
System.out.println("Not authorized");
int rc = super.prepareQueryText(dataBinder, executionContext);
System.out.println("After PrepareQueryText "+dataBinder.getLocalData());
return rc;
}

if (responses==null || responses.size()==0) {
int rc = super.prepareQueryText(dataBinder, executionContext);
System.out.println("After PrepareQueryText "+dataBinder.getLocalData());
return rc;
} else {
System.out.println("There are "+responses.size()+" responses");
}

if (queryText==null) {
queryText="";
}

if (queryText.trim().length() > 0) {

queryText+=" <AND> ";

}

queryText+=" "+oesInstallFilter.getRoleAttribute()+" <contains> `";

Iterator<String> keys = responses.keySet().iterator();

while( keys.hasNext()) {

queryText+=keys.next()+",";



}

queryText = queryText.substring(0,queryText.length()-1);
queryText+="`";

System.out.println("QueryText="+queryText);


Properties props = dataBinder.getLocalData();
props.setProperty("QueryText",queryText);




int rc = super.prepareQueryText(dataBinder, executionContext);
System.out.println("After PrepareQueryText "+dataBinder.getLocalData());
return rc;
}

The idea is that we're looking for responses, and that response are values defined for a special meta-data attribute xoespolicies that I added to the repository. Any responses I recieve, I concatenate into the list of allowable values for that attribute and append it to the QueryText data binder attribute. This new QueryText is used by the search engine (in this case OracleText) to make the correct queries. This approach avoids having to go row by row when searching and applying policies on each individual document. This example can be extended to work with other attributes of the documents.



The Resource Definition tab of the Component Wizard shows the resources for the custom IdocScript that I also created. I added a very simple function $isAccessAllowed$. It takes a single argument (the action), and retrieves the name from the UserData and the resource from the APP_ID attribute in the QUERY_STRING. The intricacies of creating custom IdocScript extensions could fill a whole new post, but I'll just focus on the relevant pieces here. From the oes.OESScriptExtensions, here's the code:

case 4: //isAccessAllowed

String appName = null;

String queryString = binder.getEnvironment().getProperty("QUERY_STRING");

List<String> parameters = Arrays.asList(queryString.split("&"));

for (String param: parameters) {

if (param.startsWith("APP_ID")) {

int posOfEq = param.indexOf("=");
appName = param.substring(posOfEq+1);

}

}


Object authenticIdentity = oesInstallFilter.getRuntime().assertIdentity("USERID_TOKEN",userData.getProperty("dName"));
System.out.println("OES USER="+authenticIdentity);
System.out.println("APPNAME="+appName);

String resource = oesInstallFilter.getResourcePrefix()+"/"+appName;
String action = sArg1;

bResult = oesInstallFilter.getRuntime().isAccessAllowed(authenticIdentity,resource,action, new Properties(), new HashMap());


args[nargs] = ScriptExtensionUtils.computeReturnObject(1,
bResult, iResult, dResult, oResult);


return true;


I think that there are lots of variations for the custom IdocScript that could call other OES API calls, or maybe once the call is made store information like the roles or privileges in other variables accessable from IdocScript. The sky is really the limit.

Summary


The samples that I've posted are just the foundation for many richer and deeper integrations between OES and UCM. I've done a lot of the heavy-lifting (classloader, configuration, basic integration) with the hope that other people will use it in their own projects. As always, I'm happy to answer questions, or take suggestions/request. Simply post them here.

Friday, February 5, 2010

Testing your WebLogic/Kerberos setup

I spent several hours today beating my head against a wall and thought I'd post this in case anyone else runs into the same problem.

I have a virtual environment with two machines, one is a KDC and one is just a WebLogic server. I setup WebLogic to do Kerberos, following the directions in the the documentation and could not figure out what I was doing wrong.

Whenever I accessed WebLogic I kept seeing the header "Authorization: Negotiate TlRMTVNTUAAB..." with only a short string as the token. If you base64 decode that string you see that it starts with NTLMSSP. In other words most definitely NOT a Kerberos token.

It looks like IE6 and IE 8 on Windows 2003 figures out that it's talking to the local machine (i.e. localhost) even if you use the fully qualified domain name!

So if you're trying to get Kerberos working make sure you use a web browser on a different machine. Or just use Firefox instead.

Thursday, February 4, 2010

Extending the Universal Content Managament (UCM) Security Model with Oracle Entitlements Server (OES) - Part 1

I've been working with a customer on setting up OES to provide authorization for WebLogic Portal (WLP) and Universal Content Management (UCM). The OES support for WLP is pretty straight-forward and can be found in the product documentation. The OES-UCM integration was a pretty trivial coding exercise, simply combining two existing product samples.

Overview


I've discussed before on the issues around data security and how to apply OES to that problem., but putting it into action, and seeing it work is very cool. Before I get into the details on the integration, I want to explain why you would need to add OES to your UCM solution. UCM has a very sophisticated security model optimized for content management, but there are at least two scenarios I can think of. The first is that you need a common model that goes beyond just UCM. UCM can externalize groups and accounts via LDAP, but some if you need a model richer than "groups", externalization via OES might make sense. The second is if you need very fine grained authorization based on rich context. The most common case is an Attribute Based Access Control (ABAC) model comparing information about users with attributes on the documents/folders in UCM.

In either one of the two scenarios, the basic philosophy is to try to use as much of the OOTB UCM Security functionality as possible, and then use OES to externalize additional security capabilities. In practice, you're effectively ANDing the permissions from OES with the permissions in UCM, so making UCM should deliver higher performance and less work in OES.

Setting up OES with UCM



  • Create an instance of the Java SM - I wanted to use the Java SM for performance reasons. This introduces some other issues, but in OES 10.1.4.3 cp3, there is a sample that demonstrates how to use a custom classloader with the Java SM. This can be very handy when integrating into Java containers because you can avoid/work around a lot of very nasty class loading conflicts.
  • Configure the UsernameIdentity Asserter in the SM - Chris called this thing the "worlds most dangerous identity asserter" and he's right, but from inside a Java container when all you have is the username, I think this will do.
  • Modify the intradoc.cfg to include the Java system properties and classpath for the instance - When the instance is created a file called BEA_HOME/ales32-ssm/java-ssm/instancename/bin/set-env.sh is created. This file contains a long list of Java system properties that need to be added to UCM environment. This is done in intradoc.cfg as follows:


    #Content Server Directory Variables
    IntradocDir=/VZ/opt/ucm/server/
    WebBrowserPath=/usr/bin/firefox

    #Additional Variables
    HTMLEditorPath=/usr/bin/gedit
    JAVA_CLASSPATH_defaultjdbc=$SHAREDDIR/classes/ojdbc14.jar
    FileEncoding=UTF8

    # Start Standard Java SM Properties
    JAVA_OPTIONS_1=-Dwles.scm.port=7063
    JAVA_OPTIONS_2=-Dwles.arme.port=7772
    JAVA_OPTIONS_3=-Dwles.config.signer=poc.us.oracle.com
    JAVA_OPTIONS_4=-Dlog4j.configuration=file:/opt/fmw/ales32-ssm/java-ssm/instance/ucm/config/log4j.properties
    JAVA_OPTIONS_5=-Dlog4j.ignoreTCL=true
    JAVA_OPTIONS_6=-Dwles.ssl.passwordFile=/opt/fmw/ales32-shared/keys/password.xml
    JAVA_OPTIONS_7=-Dwles.ssl.passwordKeyFile=/opt/fmw/ales32-shared/keys/password.key
    JAVA_OPTIONS_8=-Dwles.ssl.identityKeyStore=/opt/fmw/ales32-shared/keys/identity.jceks
    JAVA_OPTIONS_9=-Dwles.ssl.identityKeyAlias=wles-ssm
    JAVA_OPTIONS_10=-Dwles.ssl.identityKeyPasswordAlias=wles-ssm
    JAVA_OPTIONS_11=-Dwles.ssl.trustedCAKeyStore=/opt/fmw/ales32-shared/keys/trust.jks
    JAVA_OPTIONS_12=-Dwles.ssl.trustedPeerKeyStore=/opt/fmw/ales32-shared/keys/peer.jks
    JAVA_OPTIONS_13=-Djava.io.tmpdir=/opt/fmw/ales32-ssm/java-ssm/instance/ucm/work/jar_temp
    JAVA_OPTIONS_14=-Darme.configuration=/opt/fmw/ales32-ssm/java-ssm/instance/ucm/config/WLESarme.properties
    JAVA_OPTIONS_15=-Dales.blm.home=/opt/fmw/ales32-ssm/java-ssm/instance/ucm
    JAVA_OPTIONS_16=-Dwles.scm.useSSL=true
    # End standard Java properties

    # Add this so the Java SM can identify which config to use
    JAVA_OPTIONS_17=-Dwles.realm=ucm
    # The directory where the custom classloader looks for its classes
    JAVA_OPTIONS_18=-Dwles.providers.dir=/opt/fmw/ales32-ssm/java-ssm/lib/providers
    # Helpful debug flag for apache-commons-1.1.1
    JAVA_OPTIONS_19=-Dorg.apache.commons.logging.diagnostics.dest=STDOUT
    # An additional prefix for the UCM resources
    JAVA_OPTIONS_20=-Does.ucm.prefix=ucm/

    # Just log4j.jar and Apache Commons 1.1.1 go here. The rest are loaded using the custom class loader from OES
    CLASSPATH=/opt/fmw/ales32-ssm/java-ssm/lib/log4j.jar:/opt/fmw/ales32-ssm/java-ssm/lib/commons-logging-1.1.1.jar


    The classpath set-up is in two parts. The first is in the intradoc.cfg above. In order for OES to work inside of UCM, you'll need to move to apache commons logging-1.1.1. You can download the file from here. Add it to the BEA_HOME/ales32-ssm/java-ssm/lib directory. The second step is to configure the classes that the custom class loader is going to use. Copy the file BEA_HOME/ales32-ssm/java-ssm/examples/JavaAPIExample-with-customclassloader/JarFileList.txt to BEA_HOME/ales32-ssm/java-ssm/lib. From there modify the file as follows:

    /java-ssm/instance//config
    /java-ssm/lib/oesucmclassloader.jar
    /java-ssm/lib/api.jar
    /java-ssm/lib/css.jar
    /java-ssm/lib/saaj.jar
    /java-ssm/lib/framework.jar
    /java-ssm/lib/scmapi.jar
    #/java-ssm/lib/log4j.jar
    /java-ssm/lib/jmx.jar
    /java-ssm/lib/connector.jar
    /java-ssm/lib/asi_classes.jar
    /java-ssm/lib/EccpressoCore.jar
    /java-ssm/lib/EccpressoJcae.jar
    /java-ssm/lib/jsafeFIPS.jar
    /java-ssm/lib/jsafeJCEFIPS.jar
    /java-ssm/lib/sslplus.jar
    /java-ssm/lib/ssladapter.jar
    /java-ssm/lib/wlcipher.jar
    /java-ssm/lib/asitools.jar
    /java-ssm/lib/webservice.jar
    /java-ssm/lib/webserviceclient.jar
    /java-ssm/lib/org.mortbay.jetty.jar
    /java-ssm/lib/javax.servlet.jar
    /java-ssm/lib/sslserver.jar
    /java-ssm/lib/sslclient.jar
    /java-ssm/lib/pdsoap.jar
    /java-ssm/lib/antlr.jar
    /java-ssm/lib/axis.jar
    /java-ssm/lib/commons-discovery-0.2.jar
    #/java-ssm/lib/commons-logging-1.0.4.jar
    #/java-ssm/lib/commons-logging-1.1.1.jar
    /java-ssm/lib/wsdl4j-1.5.1.jar
    /java-ssm/lib/jaxrpc.jar
    /java-ssm/lib/providers/ales/xercesImpl.jar
    /java-ssm/lib/providers/ales/xml-apis.jar
    /java-ssm/lib/ld-client.jar
    /java-ssm/lib/ld-server-core.jar
    /java-ssm/lib

    Notice that I've commented out log4j.jar, and both on the commons-logging jars. Also, there is a jar file listed oesucmclassloader.jar that needs to be added to the file. You don't have this file yet, but in the next section you'll learn about how to build and finishing deploying the OES-UCM integration
  • One last thing, copy the exampleNames.xml to UCM_HOME/server/bin from ALES_SSM/java-ssm/examples/JavaAPIExample-with-customclassloader/config. This file is naming authority file used by the sample.

Building and Deploying the OES UCM Integration


The OES UCM integration is basically the combination of the Java custom-classloader example, and some of the basic UCM examples. The OES examples are shipped with the product and can be found under BEA_HOME/ales32-ssm/java-ssm/examples/JavaAPIExample-with-customclassloader. The UCM examples (HowToBundle) can be found here. I've worked the two examples together to perform some basic integration and uploaded to https://oes-ucm.samplecode.oracle.com/. I checked in the entire JDeveloper application. Its simple to build, but there are a few things that have to be done.

  • Update the oes-ucm library with the OES 10gR3cp3 and UCM 10gR3 libraries - The OES libraries are in BEA_HOME/ales32-ssm/java-ssm/lib (take all of them), and the ucm library is simply the UCM_HOME/server/shared/classes/server.zip



  • Update the deployment locations of the oescomponent and CustomClassLoader projects - The deployment target for the oescomponent project creates a deployable UCM component. The CustomClassLoader project generates a jar that gets loaded by the system classloader and works with the OES custom classloader (not the classloader itself - so bad jar name on my part ;). Modify the settings to deploy these jars to a valid locations


  • Deploy the oescomponent project - Deploy the ucm jar target. This will not only generate the oes-ucm.jar (the deployable UCM component), but it will also generate the oesucmclassloader.jar.
  • Copy the oesucmclassloader.jar to BEA_HOME/ales32-ssm/java-ssm/lib
  • Deploy the oes-ucm component to UCM - UCM_HOME/server/bin/ComponentTool -install location of oes-ucm.jar. This will create a component called oes
  • Start UCM - If everything works, you should have a Java SM running inside of UCM.

Summary


By combining the Java SM custom classloader example and some of the UCM custom examples, I've created a basic integration between OES and UCM. That integration is available on https://oes-ucm.samplecode.oracle.com. I'm looking forward to sharing more of the details of what the integration actual does in a Part 2 post, very shortly.

Kerberos and WebLogic Server on Windows step-by-step

The month of Kerberos continues

I got a frantic call late last week asking for help getting WebLogic and Kerberos working. WebLogic would be deployed on Windows but, unlike in my previous post, this customer wanted IE to talk directly to WebLogic with no IIS server in between.

Easy enough, right? It is, but there are some nuances.

There are official docs available from download.oracle.com, but some people find them confusing. These are a bit simplified and are intended for this configuration only.

In this environment the desktop and web server machines are both in the same Windows domain and thus also in the same Kerberos Domain. It's important to note that in my case the machine is part of the domain, but it doesn't actually have to be in the domain to get configuration working.

Prerequisites:
  • You are using WebLogic with JDK 1.6 (either JRockit or the Sun JDK)
  • You have the Active Directory domain created
  • You have downloaded and installed WebLogic and may have created a domain
  • You are frustrated because the docs are confusing and despite working on this for hours you still can't get Kerberos authentication working
Setup the Kerberos KDC bits...

Before you do anything else we need to make sure that you're starting in a "good" state.

Delete any keytab files you've already created. Delete any cached keys (del "%USERPROFILE%\krb5cc*"). Clear out any config files you've created. And log out and back into the account in which you're running Internet Explorer. Also make keep my previous post in mind while you're testing.

If you have installed IIS on the machine uninstall it. IIS registers the kerberos service principal HTTP/machine and HTTP/machine.domain.com and if you leave IIS installed you'll never manage to get Kerberos on WebLogic working correctly.

Install the setspn utility. Info on the utility is available on MS TechNet, and it is installed as part of the Windows Server 2003 Support Tools from the Windows product CD.

Run the command "ldifde -f c:\export.txt" on your Windows server. This will export the entire contents of your Active Directory to a flat file so we can search it. What we need to do is make sure that no user or machine has already registered the Kerberos Service Principal we need for WebLogic. If you don't know what that means don't worry about it, just search the export.txt file for HTTP/machine (where machine is the name of the machine). If you find it in the file you'll need to use the setspn utility to remove the mapping. Here's what my export contained when things were broken:
dn: CN=WEBSERVER,CN=Computers,DC=kerbtest,DC=com
servicePrincipalName: HOST/WEBSERVER
servicePrincipalName: HOST/webserver.kerbtest.com
servicePrincipalName: HTTP/webserver
servicePrincipalName: HTTP/webserver.kerbtest.com
Use the setspn utility to remove the extraneous mappings. In my example above I had a mapping from the machine named "WEBSERVER" to the two principals HTTP/webserver and HTTP/webserver.kerbtest.com. I needed to remove those mappings before continuing on to the next step. You have to use the setspn utility with the -D flag to remove the mappings like so:
C:\>setspn -d HTTP/webserver webserver
Unregistering ServicePrincipalNames for CN=WEBSERVER,CN=Computers,DC=kerbtest,DC=com
HTTP/webserver
Updated object
C:\>setspn -d HTTP/webserver.kerbtest.com webserver
Unregistering ServicePrincipalNames for CN=WEBSERVER,CN=Computers,DC=kerbtest,DC=com
HTTP/webserver.kerbtest.com
Updated object
Finally we need to create a new user in the domain so that WebLogic can get the Kerberos secret associated with the Kerberos SPN. The official docs and basically everything else you'll find on the Internet says to make the username the same as the machine name; I disagree strongly. If you give the machine and user the same name you'll just confuse yourself later trying to figure out which is which plus the setspn tool assumes you're talking about the machine and there's no way to tell it to operate on the user instead. So when you create the username make it generic, for example "wlsuser", "webuser" or "webserveruser".

Once you've created that user use the setspn utility to associate the HTTP/machine and HTTP/machine.domain.com principals with the user you just created. Yes, you are going to associate the SPN with the user and NOT with the machine. I'll explain why later, but this is really important! When you do this you want HTTP to be in all caps and the machine name to be all lower case. The capitalization is important because the format has to match what Internet Explorer uses when it talks to the KDC. Here's what my output looks like:
C:\>setspn -a HTTP/webserver webuser
Registering ServicePrincipalNames for CN=web user,CN=Users,DC=kerbtest,DC=com
HTTP/webserver
Updated object
C:\>setspn -a HTTP/webserver.kerbtest.com webuser
Registering ServicePrincipalNames for CN=web user,CN=Users,DC=kerbtest,DC=com
HTTP/webserver.kerbtest.com
Updated object
You can use ldifde to export the contents of AD again just to be sure you've got it right.

Configure the WebLogic bits...

Install WebLogic as normal.
Create a new WebLogic domain using the Config Wizard.
Make a backup of config/config.xml. Don't ask, just do it!
Open the WebLogic console (http://localhost:7001/console by default)
In the left Domain Structure navigation menu click on Security Realms, then click on myrealm.
Click the Providers tab and you should see the list of Authentication Providers.
Click the New button to create a new authentication provider and select Negotiate from the drop down.
Go into the Identity Asserter's configuration and click on the Provider Specific tab. We don't want the NegotiateIdentityAsserter to run when you deploy an application with the authentication method set to Forms or Basic, so uncheck the "Form Based Negotiation Enabled" box.

Note that once you hit save and restart WebLogic you'll probably be locked out of the console until we do the next bunch of steps. If you wind up in trouble just back out the above changes by swapping your backup of config.xml in, restarting WebLogic and your web browser.

Copy a test app that requires Certificate authentication into your autodeploy directory. Then restart your server. If you don't have such app you'll have to create one.

When you access the app you should get a 401 error page from WebLogic. If you do an HTTP trace you should see an HTTP transaction that looks something like this
HTTP/1.1 401 Unauthorized
Date: Thu, 04 Feb 2010 21:44:10 GMT
Content-Length: 1518
Content-Type: text/html
WWW-Authenticate: Negotiate
the key to look for is WWW-Authenticate: Negotiate. If you see that the Negotiate Identity Asserter is running and you can go ahead and finish up the Kerberos setup.

Back to more Kerberos stuff...

We need to create the kerberos ini and keytab files:

Create a c:\windows\krb5.ini file that looks like the following. Update the domain and other information to reflect your environment:
[libdefaults]
default_realm = KERBTEST.COM
ticket_lifetime = 600

[realms]
KERBTEST.COM = {
kdc = 10.99.2.181
admin_server = testmachine
default_domain = KERBTEST.COM
}

[domain_realm]
.kerbtest.com = KERBTEST.COM

[appdefaults]
autologin = true
forward = true
forwardable = true
encrypt = true
In the official docs and all over the Internet you will find similar files with lines that start with default_tkt_enctypes and default_tgs_enctypes. As long as you are using JDK 1.6 you should be able to leave them out. The "kdc" setting is the IP address of your KDC and the "admin_server" setting is the host name of your KDC.

Run the command "c:\Oracle\Middleware\wlserver_10.3\common\bin\commEnv.cmd" to set your path and other environment variables so that you can run the Kerberos-related tools like ktab and kinit. Then cd into your domain's directory. In my case that's c:\Oracle\Middleware\user_projects\domains\base_domain.

The Kerberos related classes included with the JDK require a config file to run properly. Basically this file tells the GSS layer which classes are used to do the actual work and provides configuration information to those classes. Again, don't worry about what any of this means, just do the same thing I do and you'll be fine. Use notepad to create a krb5login.conf file with these contents:
com.sun.security.jgss.initiate {
com.sun.security.auth.module.Krb5LoginModule required
principal="webuser@KERBTEST.COM"
useKeyTab=true
keyTab=keytab
storeKey=true
debug=true;
};

com.sun.security.jgss.accept {
com.sun.security.auth.module.Krb5LoginModule required
principal="webuser@KERBTEST.COM"
useKeyTab=true
keyTab=keytab
storeKey=true
debug=true;
};

com.sun.security.jgss.krb5.accept {
com.sun.security.auth.module.Krb5LoginModule required
principal="webuser@KERBTEST.COM"
useKeyTab=true
keyTab=keytab
storeKey=true
debug=true;
};
We need to create the keytab file I specified in the krb5login.conf file above. To create the file you use the ktab tool, and to verify its contents you use the kinit command line tool. Both of these tools come with the JDK so you want to make sure that you're using the same JDK as WebLogic is going to use. The instructions you find in the docs talk about using the ktab and kinit command line utilities directly, and if you want you can use them. I on the other hand like lots and lots of debugging information, so I prefer to invoke the same classes by running java.exe and passing a few extra command line options. Do whichever you feel more comfortable with and as long as kinit works everything is fine.

First create the keytab with kinit:
C:\Oracle\Middleware\user_projects\domains\base_domain>java.exe -Dsun.security.krb5.debug=true sun.security.krb5.internal.tools.Ktab -k keytab -a webuser@KERBTEST.COM
Password for webuser@KERBTEST.COM:abcd1234
Config name: C:\WINDOWS\krb5.ini
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 3 1 23 16 17.
>>> KeyTabEntry: key tab entry size is 46
>>> KeyTabEntry: key tab entry size is 46
>>> KeyTabEntry: key tab entry size is 54
>>> KeyTabEntry: key tab entry size is 62
>>> KeyTabEntry: key tab entry size is 54
Done!
Service key for webuser@KERBTEST.COM is saved in keytab
If you don't see ktab create a keytab file, or if the file winds up being just a few bytes then don't proceed any further since it won't work; instead go back and check the first bunch of steps - exporting the AD contents, checking the user you created, etc.

Next run kinit to insure that the keytab file is OK and that your configuration files are all setup correctly.
C:\Oracle\Middleware\user_projects\domains\base_domain>java.exe -Dsun.security.krb5.debug=true sun.security.krb5.internal.tools.Kinit -k -t keytab webuser@KERBTEST.COM
>>>KinitOptions cache name is C:\Documents and Settings\administrator.KERBTEST\krb5cc_administrator
Principal is webuser@KERBTEST.COM
>>> Kinit using keytab
>>> Kinit keytab file name: keytab
>>> KeyTabInputStream, readName(): KERBTEST.COM
>>> KeyTabInputStream, readName(): webuser
>>> KeyTab: load() entry length: 46; type: 3
>>> KeyTabInputStream, readName(): KERBTEST.COM
>>> KeyTabInputStream, readName(): webuser
>>> KeyTab: load() entry length: 46; type: 1
>>> KeyTabInputStream, readName(): KERBTEST.COM
>>> KeyTabInputStream, readName(): webuser
>>> KeyTab: load() entry length: 54; type: 23
>>> KeyTabInputStream, readName(): KERBTEST.COM
>>> KeyTabInputStream, readName(): webuser
>>> KeyTab: load() entry length: 62; type: 16
>>> KeyTabInputStream, readName(): KERBTEST.COM
>>> KeyTabInputStream, readName(): webuser
>>> KeyTab: load() entry length: 54; type: 17
Added key: 17version: 1
Added key: 16version: 1
Added key: 23version: 1
Added key: 1version: 1
Added key: 3version: 1
Ordering keys wrt default_tkt_enctypes list
Config name: C:\WINDOWS\krb5.ini
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 3 1 23 16 17.
>>> Kinit realm name is KERBTEST.COM
>>> Creating KrbAsReq
>>> KrbKdcReq local addresses for webserver are:

webserver/10.99.2.133
IPv4 address
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 3 1 23 16 17.
>>> KrbAsReq calling createMessage
>>> KrbAsReq in createMessage
>>> Kinit: sending as_req to realm KERBTEST.COM
>>> KrbKdcReq send: kdc=10.99.2.181 UDP:88, timeout=30000, number of retries =3,
#bytes=169
>>> KDCCommunication: kdc=10.99.2.181 UDP:88, timeout=30000,Attempt =1, #bytes=1
69
>>> KrbKdcReq send: #bytes read=210
>>> KrbKdcReq send: #bytes read=210
>>> reading response from kdc
>>> KDCRep: init() encoding tag is 126 req type is 11
>>>KRBError:
sTime is Sun Feb 07 14:40:09 EST 2010 1265571609000
suSec is 135477
error code is 25
error Message is Additional pre-authentication required
realm is KERBTEST.COM
sname is krbtgt/KERBTEST.COM
eData provided.
msgType is 30
>>>Pre-Authentication Data:
PA-DATA type = 11
PA-ETYPE-INFO etype = 23
>>>Pre-Authentication Data:
PA-DATA type = 2
PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
PA-DATA type = 15
Kinit: PREAUTH FAILED/REQ, re-send AS-REQ
>>>KrbAsReq salt is KERBTEST.COMwebuser
Pre-Authenticaton: find key for etype = 23
AS-REQ: Add PA_ENC_TIMESTAMP now
>>> EType: sun.security.krb5.internal.crypto.ArcFourHmacEType
>>> KrbAsReq calling createMessage
>>> KrbAsReq in createMessage
>>> Kinit: sending as_req to realm KERBTEST.COM
>>> KrbKdcReq send: kdc=10.99.2.181 UDP:88, timeout=30000, number of retries =3,
#bytes=240
>>> KDCCommunication: kdc=10.99.2.181 UDP:88, timeout=30000,Attempt =1, #bytes=2
40
>>> KrbKdcReq send: #bytes read=1243
>>> KrbKdcReq send: #bytes read=1243
>>> reading response from kdc
>>> EType: sun.security.krb5.internal.crypto.ArcFourHmacEType
>>> KrbAsRep cons in KrbAsReq.getReply webuser
New ticket is stored in cache file C:\Documents and Settings\administrator.KERBTEST\krb5cc_administrator
Notice that line "New ticket is stored". If you get that line it means that the Kerberos configuration is correct and that the Java code was able to acquire the secret key.

Last configuration step...

Edit the bin\startWebLogic.cmd to add the line set JAVA_OPTIONS below in the place I've shown it:
echo starting weblogic with Java version:

%JAVA_HOME%\bin\java %JAVA_VM% -version

set JAVA_OPTIONS=%JAVA_OPTIONS% -Dsun.security.krb5.debug=true -Djavax.security.auth.useSubjectCredsOnly=false -Djava.security.auth.login.config=krb5Login.conf -Djava.security.krb5.realm=KERBTEST.COM -Djava.security.krb5.kdc=testmachine.kerbtest.com
Restart WebLogic, access the site with IE and revel in your success.

Problems? Issues?

Hit me up in the comments!