Thursday 21 November 2013

Syncing Active Directory Groups with UCM Aliases

So I was required to find a way to drive Approvers in UCM by Active Directory groups (nightmare!) after some deliberation and a few meetings the simplest solution in my mind was a link between Active Directory Groups and UCM Aliases.

If we could link these lists together then we could have the LDAP server as the source of truth for who belongs in the Alias. And thus a component was born. I extended some CIS code from Jonathan Hult and creating a Filter on the following two services
IdcService=LOGIN
and
IdcService=GET_SEARCH_RESULTS
When a user executes these services custom java code compares the current users Groups agains the Alias list in UCM. If theres a match The user is added to the group!

heres a link to the component!

JAVA: Reading Weblogic Username/Password from the boot.properties file

In order to create a JMX connection to the WLS you must provide a username/password. If you are deploying a webapp to the server and want to read the properties you can use the following methods.

public Map init() {

Map<String, String> userCredentials = new HashMap<String, String>();
       //FIND THE PATH THIS CLASS FILE IS DEPLOYED TO
      Class cls = this.getClass();
      String path = cls.getProtectionDomain().getCodeSource().getLocation().getPath();
      String decodedPath = "FAIL";
       //STRIP THE PATH BACK TO /servers/
try {
              decodedPath = URLDecoder.decode(path, "UTF-8");
             decodedPath = decodedPath.substring(0, decodedPath.indexOf("/servers/"));
     } catch (Exception ex) {
             System.out.println(ex);
     }
//Change domain to aserver from mserver -- DEPENDING ON YOUR DOMAIN SETTINGS
      String domain = decodedPath.replaceAll("mserver", "aserver");

      //GET A STRING OF THE CONTENTS OF THE BOOT.PROPERTIES USING THE readBootProperties method
            String[] wlsbootProperties = readBootProperties(domain);
            String username = "";
            String password = "";
            //Find Username / Password strings
                     for (int i = 0; i < wlsbootProperties.length; i++) {
                if (wlsbootProperties[i].indexOf("username") > -1) {
                    username = wlsbootProperties[i];
                } else if (wlsbootProperties[i].indexOf("password") > -1) {
                    password = wlsbootProperties[i];
                }
            }

            //Strip back to GET AES STRINGS
            username =
                    username.substring(username.indexOf("{AES}"), username.length());
            password =
                    password.substring(password.indexOf("{AES}"), password.length());


            try { //CLEAN OUT BACKSLASHES
                while (username.indexOf("\\") > -1) {
                    username = username.replace("\\", "");
                }
                while (password.indexOf("\\") > -1) {
                    password = password.replace("\\", "");
                }
                           //DECRYPT
                String decryptUser =
                    new weblogic.security.internal.encryption.ClearOrEncryptedService(weblogic.security.internal.SerializedSystemIni.getEncryptionService(domain)).decrypt(username);
                           //DECRYPT
                String decryptPass =
                    new weblogic.security.internal.encryption.ClearOrEncryptedService(weblogic.security.internal.SerializedSystemIni.getEncryptionService(domain)).decrypt(password);
                           //STORE & Return
                userCredentials.put("username", decryptUser);
                userCredentials.put("password", decryptPass);

                return userCredentials;

            } catch (Exception ex) {
                System.out.print(ex);


            }
       
        return userCredentials;

    }

/**
     *  reads the boot properties from the domaindir
     */
    public String[] readBootProperties(String domainDir) {
        String str = "";
        File file =
            new File(domainDir + "/servers/AdminServer/security/boot.properties");
        FileInputStream fis = null;

        try {
            fis = new FileInputStream(file);

            int content;

            while ((content = fis.read()) != -1) {
                // convert to char and display it
                str += (char)content;
                //System.out.print((char) content);
            }


        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null)
                    fis.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        return str.split("\n");
    }
Depending on your domain installation you might need to change a few strings to get the
readBootProperties(String domainDir)
Reading the correct files

Wednesday 20 November 2013

Pulling LDAP attributes out of WLS Active Directory Provider

I recently had to connect to the WLS Active Directory Provider in order to execute some custom Java logic. During my research I happened upon the following blog entry Creating Utilities to Manipulate Users in #Weblogic Using #JMX and #MBeans Which worked perfectly for pulling down usernames and user groups but it seemed to be lacking with respect to pulled down LDAP attributes. I mucked around and came up with the following solution.

To pull the LDAP providers hostname out of the WLS Authentication Provider

Add a method at the bottom of the Utilities class
        public static String getUserDetail(String user, String detail) {
         

            try {
                // Set up the environment for creating the initial context
                Hashtable env = new Hashtable();
                env.put(Context.INITIAL_CONTEXT_FACTORY,
                    "com.sun.jndi.ldap.LdapCtxFactory");
                //CREATE LDAP CONNECTION
                env.put(Context.PROVIDER_URL, "ldap://"+connection.getAttribute(defaultAuthenticator, "Host")+":389/");

                // Authenticate
                env.put(Context.SECURITY_AUTHENTICATION, "simple");
                // PULL PRINCIPAL FROM WLS
                env.put(Context.SECURITY_PRINCIPAL, connection.getAttribute(defaultAuthenticator, "Principal"));
                // UNFORTUNATELY HAD TO HARDCODE CREDENTIAL CAN POSSIBLY MOVE TO CONFIG or WEB.XML
                env.put(Context.SECURITY_CREDENTIALS, "Credential");
                DirContext ctx = new InitialDirContext(env);
                String[] attrIDs = { "sAMAccountName", "cn", "title", "mailnickname", "mail", "manager", "department", "telephoneNumber" };
                SearchControls ctls = new SearchControls();
                ctls.setReturningAttributes(attrIDs);
                ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
                NamingEnumeration<SearchResult> answer = ctx.search("LDAP OU", "(&(objectCategory=person)(objectClass=user)(sAMAccountName="+user+"))", ctls);
                while (answer.hasMore()) {
                SearchResult sr = (SearchResult) answer.next();
                    return sr.getAttributes().get(detail).toString();
                }


            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

and voila! We now have a method for pulling out user attributes (mail, manager, title etc) I believe there may be a better way that utilizes JPS which would hopefully circumvent the inclusion of the Credential in my method.

Selecting a Workflow Approver using Tokens & Multiple Aliases

I was given the requirement to create a workflow which could be re-used across several different departments. From the get-go I defined this could be achieved by using:

  • 1 x Token (Open WF Admin -> Options -> Tokens)
  • 1 x Alias for each Department
  • 1 x Alias for each Workflow Step (eg if the WF has 3 steps in document approval than 3 x Aliases)
Now I initially assumed that if I created a Token and added two of the
<$wfAddUser(dWfStepName, "alias")$>
<$wfAddUser(xDepartment, "alias")$>
Statements that it would create an intersection of the Aliases, but instead it just acts as a Union! 

But alas you can write IdocScript within the Token! which allows us to programmatically utilize <$wfAddUser(UCMUSER, "user")$>

So I wrote the following script

//SET ALIASES TO BE COMPARED
<$WFALIAS1NAME = "dWfStepName"$>
<$WFALIAS2NAME = "dDepartment"$>
//STRING TO STORE USERS FOR COMPARISON
<$WFUSERSTRING = ""$>
//EXECUTE SERVICE TO LOAD ALL ALIASES & THEIR USERS
<$executeService("GET_ALIASES")$>
<$name="AliasUserMap"$>
<$var = rsFirst(name)$>
<$loopwhile getValue(name, "#isRowPresent")$>
    <$alias = getValue(name, "dAlias")$>
    <$user = getValue(name, "dUserName")$>
  //IF ALIAS RS Equals the Alias we are searching for
  <$if WFALIAS1NAME LIKE alias$>
        //ADD USER FROM ALIAS MATCH 1 TO USERSTRING
       <$WFUSERSTRING = WFUSERSTRING & "," & user$>
  <$endif$>


    <$var = rsNext(name)$>
<$endloop$>

//START CHECKING THE SECOND ALIAS
<$executeService("GET_ALIASES")$>
<$name="AliasUserMap"$>
<$var = rsFirst(name)$>
<$loopwhile getValue(name, "#isRowPresent")$>
    <$alias = getValue(name, "dAlias")$>
    <$user = getValue(name, "dUserName")$>
  //IF ALIAS RS Equals the Alias we are searching for
  <$if WFALIAS2NAME LIKE alias$>
       //IF THE USER IS IN THIS ALIAS & HAS A MATCH IN USER STRING ADD TO WF
       <$if strIndexOf(WFUSERSTRING,user) !=-1 $>
            <$wfAddUser(user, "user")$>
       <$endif$>
  <$endif$>
    <$var = rsNext(name)$>
<$endloop$>


The script adds the user by looping the first alias creating a string of users in that alias, then looping the second alias and searching for matching users.

jiri.machotka commented on my OTN Post recommended
"rsMergeReplaceOnly() function (see Idoc Script Functions and Variables - 11g Release 1 (11.1.1)). You could create an intersection of the two aliases (by two calls of the function), and then add all remaining users in one loop."

Which sounds like a much cleaner solution that I intend to implement.

Monday 18 November 2013

Bulk / Mass Update Content via RIDC

As Mass Metadata updating cannot be done with Repository Manager and Archiver can be arduous the following Java Class can be used instead. (Change the red text to suit your circumstances)

public class MassUpdater {
    private static IdcClientManager manager = new IdcClientManager();
    private static IdcClient idcClient;
    private static IdcContext userContext;

 public static boolean massUpdate() {

        DataBinder binder = idcClient.createBinder();
        binder.putLocal("IdcService", "GET_SEARCH_RESULTS");
        binder.putLocal("QueryText",
                        "dSecurityGroup <matches> `Public`");
        binder.putLocal("searchFormType", "standard");
        binder.putLocal("SearchQueryFormat", "UNIVERSAL");
        binder.putLocal("ftx", "");
        binder.putLocal("AdvSearch", "True");
        binder.putLocal("folderChildren", "");
        binder.putLocal("ResultCount", "200");
        binder.putLocal("SortField", "dInDate");
        binder.putLocal("SortOrder", "Desc");

        try {
            ServiceResponse response =
                idcClient.sendRequest(userContext, binder);
            DataBinder serverBinder = response.getResponseAsBinder();
              DataResultSet resultSet =
                serverBinder.getResultSet("SearchResults"); // loop over the results
         
            int z = 0;
            for (DataObject dataObject : resultSet.getRows()) {
                singleUpdate(dataObject.get("dDocName"),dataObject.get("dID"),dataObject.get("dRevLabel"),dataObject.get("dSecurityGroup"), "UCMMETADATAFIELD", "UCMMETADATAVALUE");
                z++;
                System.out.println(dataObject.get("dDocTitle")+"----"+z+"/"+resultSet.getRows().size());
                
            }
            return true;

        } catch (Exception ex) {
            System.out.println(ex);
        }

        return false;
    }

 public static boolean singleUpdate(String dDocName, String dID,
                                       String dRevLabel,
                                       String dSecurityGroup, String field, String value) {

        DataBinder binder = idcClient.createBinder();
        binder.putLocal("IdcService", "UPDATE_DOCINFO");
        binder.putLocal("dDocName", dDocName);
        binder.putLocal("dID", dID);
        binder.putLocal("dRevLabel", dRevLabel);
        binder.putLocal("dSecurityGroup", dSecurityGroup);
        binder.putLocal(field, value);
        

        try {
            ServiceResponse response =
                idcClient.sendRequest(userContext, binder);
            DataBinder serverBinder = response.getResponseAsBinder();
            //System.out.println(serverBinder.getLocalData().get("dStatus"));

            return true;

        } catch (Exception ex) {
            System.out.println(ex);
        }

        return false;
    }



     public static void init() {
        try {
            String ucmServer = "ucmServerURL";

            MassUpdater.idcClient =
                    MassUpdater.manager.createClient("idc://" + ucmServer +
                                                     ":4444");
            MassUpdater.idcClient.getConfig().setSocketTimeout(30000); // 30 seconds
            MassUpdater.idcClient.getConfig().setConnectionSize(20);
            userContext = new IdcContext("sysadmin");

        } catch (Exception ex) {
            System.out.println(ex);
        }

    }
}

You may want to extend this to take a Map of Metadata/Value pairs if you want to achieve true re usability.
You should also Paginate your results depending on your MaxQueryRows / ResultCount