Saturday, July 26, 2014

Deployment Script for Webcenter Application


Oracle WebCenter Portal applications differ from traditional Java EE applications in that they support run-time customization. WebCenter Portal application customizations are stored in Oracle Metadata Services (MDS), which is installed in a database/file. So, while deploying the webcenter application on WLS, you need to  specify the metadata repository and the partition in the repository that application will be deployed to.
As far as I know, targeting the application to MDS repository deployment is not possible using the wldeploy ant task. WLST commands can be used  to target the application to MDS repository and deploy the application on WLS. So, first create a python script and then call it from the ant script.

Here is the script deploy.py -

# Below args are passed by the ant script
adminUser=sys.argv[1]
adminPassword=sys.argv[2]
adminUrl=sys.argv[3]
# Connect to WLS
connect(adminUser,adminPassword,adminUrl)
domainRuntime()

# Stop the application
def stop():
 stopApplication(sys.argv[6])

# Undeploy the applciation
def undeployApp():
 print 'Begin undeploy'
 undeploy(sys.argv[6])
 print 'End undeploy'

# Deploy the application
def deployApp():
 print 'Begin deployAll'
 serverConfig()
 # Returns a handle to the MDSArchiveConfig object for the specified archive 
 archive = getMDSArchiveConfig(fromLocation=sys.argv[4])
 # Sets the connection details for the application metadata repository
 archive.setAppMetadataRepository(repository=sys.argv[5],partition=sys.argv[6],
 type=sys.argv[7], jndi=sys.argv[8])
 archive.save()
 deploy(appName=sys.argv[6], path=sys.argv[4], targets=sys.argv[9], upload='true')
 
 print 'End deployAll' 

# Start the application 
def start(): 
 startApplication(sys.argv[3])
 
# Deploy script init- First Stop the application and then undeploy. 
#After undeploying, deploy the application and restart

try:
 try:
  stop()
 except:
  print sys.exc_info()[0]
 print 'Stop Done'
 try:
  undeployApp()
 except:
  print sys.exc_info()[0]
 print 'Undeploy Done'
 deployApp()
 start()
 
except:
 print 'Unexpected error: ', sys.exc_info()[0]
 dumpStack()
 raise


Build.xml
See the target "deploy-to-dev-server" to call the above python script.

<?xml version="1.0" encoding="US-ASCII" ?>
<project name="DigitalFirst" default="all" basedir=".">
<property environment="env"/>
<property file="build.properties"/>

<taskdef name="ojdeploy"
           classname="oracle.jdeveloper.deploy.ant.OJDeployAntTask"
           uri="oraclelib:OJDeployAntTask"
           classpath="${oracle.jdeveloper.ant.library}"/>

 <!-- This target is to build ear for application -->   
  <target name="deploy-to-ear" depends="deploy-all-projects">
    <ora:ojdeploy xmlns:ora="oraclelib:OJDeployAntTask"
                  executable="${oracle.jdeveloper.ojdeploy.path}"
                  ora:buildscript="${oracle.jdeveloper.deploy.dir}/ojdeploy-build.xml"
                  ora:statuslog="${oracle.jdeveloper.deploy.dir}/ojdeploy-statuslog.xml">
      <ora:deploy>
        <ora:parameter name="workspace"
                       value="${oracle.jdeveloper.workspace.path}"/>
        <ora:parameter name="profile" value="*"/>
        <ora:parameter name="outputfile"
                       value="${oracle.jdeveloper.workspace.dir}/deploy/${ear.filename}"/>
      </ora:deploy>
    </ora:ojdeploy>
  </target>

  <!-- This target is to deploy the application on WLS server  -->  
  <target name="deploy-to-dev-server" >
    <exec executable="${oracle.wlst.path}"
          spawn="false" failonerror="true">
      <!-- Python Script Relative Path from build.xml.In this case both
      are in same folder-->
      <arg value="deploy.py"/>
      <!-- Application ear Path -->
      <arg value="${ear.location}"/>
      <!-- MDS Repository Name -->
      <arg value="${dev.wc.mds.repository}"/>
      <!-- Application Name. See Project Properties -> Java EE Application -->
      <arg value="${application.name}"/>
      <!-- Repository type (DB/file) -->
      <arg value="${dev.wc.mds.repository.type}"/>
      <!-- Repository JNDI. See WLS EM console -->
      <arg value="${dev.wc.mds.repository.jndi}"/>
      <!-- WLS credential and URL -->
      <arg value="${dev.wls.username}"/>
      <arg value="${dev.wls.password}"/>
      <arg value="${dev.wls.adminurl}"/>
      <arg value="${dev.wls.cluster.name}"/>
    </exec>
  </target>


Build.properties

Below is the same build.properties file. Set the paramters based on your environment -

# Oracle Jdev Library Path variables
oracle.middleware.home=<Middleware Home> e.g C:/Oracle/Middleware
oracle.jdeveloper.home=${oracle.middleware.home}/jdeveloper
oracle.wls.home=${oracle.middleware.home}/wlserver_10.3
oracle.wlst.path=${oracle.middleware.home}/oracle_common/common/bin/wlst.cmd
oracle.jdeveloper.ant.library=${oracle.jdeveloper.home}/jdev/lib/ant-jdeveloper.jar
oracle.jdeveloper.ojdeploy.path=${oracle.jdeveloper.home}/jdev/bin/ojdeploy.exe

# Worspace related
oracle.jdeveloper.workspace.dir=<Workspace Directory> e.g C:/MyProjects/PortalApplication
oracle.jdeveloper.workspace.path=${oracle.jdeveloper.workspace.dir}/DigitalFirstApp.jws

# Deployment and ear related parameters
oracle.jdeveloper.deploy.dir=${oracle.jdeveloper.workspace.dir}/deploy
application.name=PortalApplication
ear.filename=PortalApplication.ear
ear.location=${oracle.jdeveloper.deploy.dir}/PortalApplication.ear

# Set the WLS environment related parameters
dev.wls.username=<weblogic admin username>
dev.wls.password=<weblogic admin password>
dev.wls.adminurl=<HostName>:<port>
dev.wls.cluster.name=<WLS Cluster name on which application is deployed>

# Set the MDS related parameters
dev.wc.mds.repository=<MDS Repository Name> e.g mds-CustomPortalDS
dev.wc.mds.repository.type=< It can be DB/file>
dev.wc.mds.repository.jndi=<Repository JNDI> e.g jdbc/mds/CustomPortalDS



Monday, July 21, 2014

Authentication against Active Directory with Java and capturing error code

Recently, I had a requirement to authenticate against active directory using Java. 
Summary of steps in the process of authentication are - 

1. Set the AD Host Name URL 

2. Set the read only username/credential to connect to AD. This is required because you can bind to AD only if you know the full distinguished name of the user.So, first you need the read only user with full distinguished name to connect to AD. 

3. Set the username attribute for ex: sAMAccountName. This attribute value is the login name for the user. It can be set to any possible User Naming attributes. Check Specifying User Name attributes 

4. Set the search related parameters e.g. search scope, search base dn and user object class. Check Specifying the Search Scope 

5. Bind the read only user and if binding successful proceed further. 

6. Create a Search Control object passing the filter and constraint. Search for the username and if found get the DN(Distinguished Name) for the user. 

7. Bind the username DN and password and check if authentication is successful 8. Return the error code by capturing it from the exception message. Possible error codes are - 

USERNAME_NOT_FOUND = 0x525; 
INVALID_PASSWORD = 0x52e; 
NOT_PERMITTED = 0x530; 
PASSWORD_EXPIRED = 0x532; 
ACCOUNT_DISABLED = 0x533; 
ACCOUNT_EXPIRED = 0x701; 
PASSWORD_NEEDS_RESET = 0x773; 
ACCOUNT_LOCKED = 0x775;


package com.usfoods.df.auth.ad;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;

import java.util.regex.Matcher;

import java.util.regex.Pattern;

import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.OperationNotSupportedException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

import oracle.jbo.JboException;

public class ADAuthenticator {
  private static final String INITCTX = "com.sun.jndi.ldap.LdapCtxFactory";
  private static final String AD_HOSTNAME = "AD_HOST";
  private static final String AD_PORT= "AD_PORT"; //Default is 389
  
  //Read Only user to connect to AD
  private static final String AD_SECURITY_PRINCIPAL="CN=readOnlyUser,DC=org,DC=local";
  
  //Read only user password to make a coonection to AD
  private static final String AD_SECURITY_PASSWORD="password";
  
  //Username attribute specified in Active Directory, it can be any user attribute
  private static final String AD_USERNAME_ATTR="sAMAccountName";
  
  //Search scope - Possible Value (base, one-level, subtree)
  private static final String AD_SEARCH_SCOPE="subtree";
  
  //Object class for the User
  private static final String AD_USER_OBJECT_CLASS="user";
  
  //Search base DN 
  private static final String AD_USER_BASE_DN="DC=employee,DC=org,DC=local";
  
  //Search Scopes
  private static final String ONELEVEL_SCOPE = "onelevel";
  private static final String SUBTREE_SCOPE = "subtree";
  
  //Error Codes
  private static final int USERNAME_NOT_FOUND = 0x525;
  private static final int INVALID_PASSWORD = 0x52e;
  private static final int NOT_PERMITTED = 0x530;
  private static final int PASSWORD_EXPIRED = 0x532;
  private static final int ACCOUNT_DISABLED = 0x533;
  private static final int ACCOUNT_EXPIRED = 0x701;
  private static final int PASSWORD_NEEDS_RESET = 0x773;
  private static final int ACCOUNT_LOCKED = 0x775;

  private static final Pattern ERROR_MESSAGE_PATTERN = Pattern.compile(".*\\s([0-9a-f]{3}).*");
  
  public ADAuthenticator() {
    super();
  }

  public int authenticate(String username, String password) {
    int errorCode;
    DirContext ctx = null;
    NamingEnumeration results = null;

    try {
      // Setting environment variables for read only user to connect to AD for search
      Hashtable adminEnv = new Hashtable();
      adminEnv.put(Context.INITIAL_CONTEXT_FACTORY, INITCTX);
      adminEnv.put(Context.PROVIDER_URL,
                   "ldap://" + AD_HOSTNAME + ":" + AD_PORT);
      adminEnv.put(Context.SECURITY_PRINCIPAL, AD_SECURITY_PRINCIPAL);
      adminEnv.put(Context.SECURITY_CREDENTIALS, AD_SECURITY_PASSWORD);
      adminEnv.put(Context.REFERRAL, "follow");

      // Binding to LDAP directory using read only user
      ctx = new InitialDirContext(adminEnv);

      //Search for the logged in user
      SearchControls constraints = new SearchControls();
      // Set search scope
      String searchScope = AD_SEARCH_SCOPE;
      if (ONELEVEL_SCOPE.equals(searchScope)) {
        constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
      } else if (SUBTREE_SCOPE.equals(searchScope)) {
        constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
      }

      String userNameAttr = AD_USERNAME_ATTR;
      String userObjectClass = AD_USER_OBJECT_CLASS;
      //Create search filter based on username attribute and object class
      String searchFilter =
        "(&(objectClass=" + userObjectClass + ")(" + userNameAttr + "=" +
        username + "))";
      
      //Apply search filter and contraints and get the search results
      results = 
          ctx.search(AD_USER_BASE_DN, searchFilter, constraints);


      String dn = "";
      List dnList = new ArrayList();
      // Fetch the DN for the given username
      while (results != null && results.hasMore()) {
        SearchResult sr = (SearchResult)results.next();
        dn = sr.getName() + ", " + AD_USER_BASE_DN;
        dnList.add(dn);

      }

      // If no user exist with the given username then throw User not found error
      if (dnList.size() == 0 || dnList.size() > 1) {
        errorCode = USERNAME_NOT_FOUND;
        return errorCode;
      }
      
      // If username exist then bind using the given password
      else if (dnList.size() == 1) {
        Hashtable env = new Hashtable();
        env.put(Context.INITIAL_CONTEXT_FACTORY, INITCTX);
        env.put(Context.PROVIDER_URL,
                "ldap://" + AD_HOSTNAME + ":" + AD_PORT);
        env.put(Context.SECURITY_PRINCIPAL, dnList.get(0));
        env.put(Context.SECURITY_CREDENTIALS, password);
        ctx = new InitialDirContext(env);
      }
    } catch (NamingException e) {
      if ((e instanceof AuthenticationException) ||
          (e instanceof OperationNotSupportedException)) {
        //Parse the error message and return the error code
        return handleNamingException(e);
      } else {
        //Handle the other excetions
      }
    }
     finally {
      if (results != null) {
        try {
          results.close();
        } catch (Exception e) {
          // Never mind this.
        }
      }
      if (ctx != null) {
        try {
          ctx.close();
        } catch (Exception e) {
          // Never mind this.
        }
      }
    }

    return errorCode;
  }
  
  private int handleNamingException(NamingException exception) {
    int errorCode = parseSubErrorCode(exception.getMessage());
    //Log the error message
    System.out.println(subCodeToLogMessage(errorCode));
    return errorCode;
  }
  
  private String subCodeToLogMessage(int code) {
      switch (code) {
          case USERNAME_NOT_FOUND:
              return "User was not found in directory";
          case INVALID_PASSWORD:
              return "Supplied password was invalid";
          case NOT_PERMITTED:
              return "User not permitted to logon at this time";
          case PASSWORD_EXPIRED:
              return "Password has expired";
          case ACCOUNT_DISABLED:
              return "Account is disabled";
          case ACCOUNT_EXPIRED:
              return "Account expired";
          case PASSWORD_NEEDS_RESET:
              return "User must reset password";
          case ACCOUNT_LOCKED:
              return "Account locked";
      }

      return "Unknown (error code " + Integer.toHexString(code) +")";
  }
  
  /**
   * This method parse the error code from the exception message and return the error code
   * For Ex: Authentication Exception is of the format - 
   * "[LDAP: error code 49 - 80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 773, vece"
   * 773 is the Hex Error Code that need be captured
   * Refer the java Matcher and Pattern classes for capturing the groups in the matched string
   * @param message
   * @return
   */
  private int parseSubErrorCode(String message) {
      Matcher m = ERROR_MESSAGE_PATTERN.matcher(message);

      if (m.matches()) {
          //Error code is in hex, so parse it for base 16
          return Integer.parseInt(m.group(1), 16);
      }

      return -1;
  }
}

Thursday, July 17, 2014

Allow unauthenticated/public access to web resources in ADF/Webcenter application

Overview

This post is about configuring public access to web resources like images, javascript, css, fonts etc in ADF/Webcenter application.

Assumption

Adf security already configured for the application.

Implementation

There are two ways to implement security in any ADF/Webcenter application, one is the container-managed security and the other is the ADF security. Container managed security is common for any J2EE web application.

Using ADF secuirty you can secure taskflows and databound web pages (having page definition) by configuring grants in jazn-data.xml.Now, lets say you want to allow public access to images in the folder /Portal/public_html/images/*. I am not sure if it is possible to do with ADF security. I  define the new security constraint in  application web.xml(Portal/public_html/WEB_INF/web.xml).

Steps to create security constraint -

1. Open web.xml and click on Security Tab.




2. Click on create icon and create a new Security Constraint for public resources.


3. Under Web Resource Collection add the web resource name as "Public Images". Add the URL Pattern relative to public_html folder e.g /images/*. Select all HTTP Methods.


You can define multiple URL patterns under the same web resource name or group URL patterns under different  web resource names.


4. Under Authorization don't select any weblogic mapped role.
Note: If ADF security is enabled, by default valid-users role created in public_html/WEB-INF/weblogic.xml and mapped to weblogic default group users. All the authenticated users get the valid-users role.



Done.
Now, you can see without authentication you can access all the images under public_html/WEB-INF/images folder.



Tuesday, July 1, 2014

Performance: Filtering Navigation Links in Webcenter Navigation Model

There are two ways to filter navigation links in Webcenter Navigation Model -

1. Resource-Level filtering - This is done by specifying the visible property for a navigation resource which determines whether the resource will be displayed or not at run time. You can also specify EL expression for visible property.



2. Catalog-Level Filtering - This is done by defining the class that implements CatalogDefinitionFilter(oracle.adf.rc.spi.plugin.catalog.CatalogDefinitionFilter).
Implement the api includeInCatalog that takes CatalogElement(Navigation resource) as a parameter. This api called for every resource in NavigationModel. If the api returns true then only resource will be visible in the NavigationModel. So, check the resource id(CatalogElement.getId()) and based on conditions return true or false.

 

Check Oracle Document for more details.

I debugged the code to find out how and when filter conditions are evaluated based on the filtering level.Here are some of the scenarios where we can make it more performant.

Resource Level Filtering


Scenario 1 - 
Visible condition result not changing for a user session

Buseiness Usecase - Based on user type show/hide navigation links. So, on login check the business logic for user type and after that no need to evaluate it again until user logout and new user login.



In Resource Level Filtering visible property is evaluated for each request. So, it's better to call the business logic only once to evaluate the visible condition assuming the visible condition evaluate to same value for a user session.
In this case you can save the result of the visible condition  in session scope variable.Now business logic will be called only once on login and after that it will check the session scope variable.

Implementation :

1. Set the visible property in the navigation model





 2. Create a Managed Bean SessionInfo and register it in adfc-config.xml with session scope.



3. In SessionInfo Bean create a Boolean property financeVisible






Idea here is not to call the logic in getter method of visible property every time. Other way to implement is to set the visible property in the login initialization logic.

Scenario 2 - 
Visible condition result may change based on business logic in a user session.

Business Usecase - For a shopping portal , user can select the departments and on the basis of selected department show/hide some navigation links.

In this case reset the session variable when user select the new department. I found Navigation Model don't refresh even if visible conditions changed. So, refresh the Navigation Model explicitly by invalidating the cache.

Code Snippet to invalidate cache -

SiteStructureContext ctx = SiteStructureContext.getInstance(); 
SiteStructure model = ctx.getDefaultSiteStructure(); 
model.invalidateCache();  
 

Catalog Level Filtering

How and When it is called ? For each navigation resource, CatalogDefinitionFilter.includeInCatalog method is called for every request. 

Scenarios where it can be used

#1 Same visible condition applied to most of the navigation links.

As the includeInCatalog method is called for each resource, it's better to use resource level filtering. But if the same visible condition is used for most of the navigation link then it's better to use Catalog level because in this way code is more readable and mangeable.
For Ex: For a Payments Only User disable all the navigation links except the Payments Page.

Implementation

  public boolean includeInCatalog(CatalogElement catalogElement,
                                  Hashtable hashtable) {
    //Get the user type
    String userType = Util.getUserInfo().getUserType();
    //If user is payment only and the navigation link id is payments page show it
    //otherwise hide it
    if("PaymentsOnly".equals(userType) && catalogElement.getId().equalsIgnoreCase("paymentsPage")){
        return true;

      }
    return false;
  }