Thursday, February 8, 2018

Best Practices for Managing User Import into Alfresco from Active Directory


Alfresco has a built-in user authentication and directory service but it can also make use of Active Directory to not only handle authentication but also handle user and group memberships. In order for this to work though, you will need to add LDAP-AD as an authentication instance in the authentication.chain setting and enable the synchronization subsystem. There are a number of settings that need to be modified in order to do this but before doing so, one needs to think out how they are going to use Active Directory to manage this.

Often when I help an Alfresco administrator set up synchronization the admin has settings that are very generic like these:


ldap.synchronization.groupSearchBase=dc\=someco,dc\=com
ldap.synchronization.userSearchBase=dc\=someco,dc\=com

ldap.synchronization.groupQuery=objectclass\=group
ldap.synchronization.personQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512))

ldap.synchronization.personDifferentialQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(!(modifyTimestamp<\={0})))
ldap.synchronization.groupDifferentialQuery=(&(objectclass\=group)(!modifyTimestamp<\={0}))


Technically speaking, these queries are valid but the end result is that setting it like this will import users and groups from a very plain perspective and could make user management more difficult than it has to be. What these queries and settings will do is this:

The search for groups and users will take place in the someco.com domain which is good.


ldap.synchronization.groupSearchBase=dc\=someco,dc\=com
ldap.synchronization.userSearchBase=dc\=someco,dc\=com


Based on these two queries however, Alfresco will import ALL users and ALL groups inside of someco.com.


ldap.synchronization.groupQuery=objectclass\=group
ldap.synchronization.personQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512))


It's very likely that this is not what you want.

Of course, a differential sync will import the same set of users (but only those that have changed since the last import):


ldap.synchronization.personDifferentialQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(!(modifyTimestamp<\={0})))
ldap.synchronization.groupDifferentialQuery=(&(objectclass\=group)(!modifyTimestamp<\={0}))


Now, you may have a use-case that requires all of your users in Active Directory. It's possible that you have a small company but be aware that this method does not encourage scalability and allow for filtering out those users in your company who will not need to use Alfresco at all.

Here is what I recommend instead. The best practice is to create an Alfresco organizational unit and then create a few Alfresco groups in this OU. For example:

Create an Alfresco organizational unit with a distinguished name:


OU=Alfresco,DC=someco,DC=com


Then you can create any Alfresco specific groups within the Alfresco OU:


CN=AlfrescoAdmins,OU=Alfresco,DC=someco,DC=com
CN=AlfrescoUsers,OU=Alfresco,DC=someco,DC=com


Once these groups are set up in Active Directory, you can assign your users to them. For my testing purposes I created 3 AD users (aduser1, aduser2, aduser3) and then made aduser1 a member of AlfrescoAdmins and then all three, members of the AlfrescoUsers group. Now we can change our queries so that only these users in these two groups are imported into Alfresco:


ldap.synchronization.userSearchBase=ou\=alfresco,dc\=someco,dc\=com
ldap.synchronization.groupSearchBase=ou\=alfresco,dc\=someco,dc\=com


Also, because we have now set our groupSearchBase to only look in the Alfresco OU, our groupQuery will be very simple:


ldap.synchronization.groupQuery=objectclass\=group


If we use the personQuery setting below, it will only import users who are members of AlfrescoAdmins and AlfrescoUsers and not every single user in Active Directory:


ldap.synchronization.personQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(|(memberOf=cn\=AlfrescoAdmins,ou=alfresco,dc=someco,dc=com)(memberOf=cn\=AlfrescoUsers,ou=alfresco,dc=someco,dc=com)))


Next of course, for differential imports, we’ll use the same queries except add the modifiedTimestamp directive at the end to ensure we only pick up changes to our users since our last import:


ldap.synchronization.personDifferentialQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(|(memberOf=cn\=AlfrescoAdmins,ou=alfresco,dc=someco,dc=com)(memberOf=cn\=AlfrescoUsers,ou=alfresco,dc=someco,dc=com))(!(modifyTimestamp<\={0})))

ldap.synchronization.groupDifferentialQuery=(&(objectclass\=group)(!modifyTimestamp<\={0}))


To tie it all together, at a glance, here are the settings I use for testing Active Directory:


### AD authentication only ###
authentication.chain=alfrescoNtlm1:alfrescoNtlm,ldap-ad1:ldap-ad
ldap.authentication.active=true
ldap.authentication.allowGuestLogin=true
ldap.authentication.userNameFormat=%s@someco.com
ldap.authentication.java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory
ldap.authentication.java.naming.provider.url=ldap://someco.com:389 # This points to your Active Directory server - IP Address is ok to use here as well!
ldap.authentication.java.naming.security.authentication=simple
ldap.authentication.escapeCommasInBind=false
ldap.authentication.escapeCommasInUid=false
ldap.authentication.defaultAdministratorUserNames=Administrator

ldap.synchronization.active=true
ldap.synchronization.java.naming.security.principal=administrator@someco.com
ldap.synchronization.java.naming.security.credentials=Alfr3sc0
ldap.synchronization.queryBatchSize=1000
ldap.synchronization.attributeBatchSize=1000
synchronization.synchronizeChangesOnly=false
synchronization.allowDeletions=true
synchronization.syncWhenMissingPeopleLogIn=true

ldap.synchronization.groupQuery=objectclass\=group
ldap.synchronization.groupDifferentialQuery=(&(objectclass\=group)(!(modifyTimestamp<\={0})))

ldap.synchronization.personQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(|(memberOf=cn\=AlfrescoAdmins,ou=alfresco,dc=someco,dc=com)(memberOf=cn\=AlfrescoUsers,ou=alfresco,dc=someco,dc=com)))

ldap.synchronization.personDifferentialQuery=(&(objectclass\=user)(userAccountControl\:1.2.840.113556.1.4.803\:\=512)(|(memberOf=cn\=AlfrescoAdmins,ou=alfresco,dc=someco,dc=com)(memberOf=cn\=AlfrescoUsers,ou=alfresco,dc=someco,dc=com))(!(modifyTimestamp<\={0})))

ldap.synchronization.groupSearchBase=ou\=alfresco,dc\=someco,dc\=com

ldap.synchronization.userSearchBase=dc\=someco,dc\=com

ldap.synchronization.modifyTimestampAttributeName=modifyTimestamp
ldap.synchronization.timestampFormat=yyyyMMddHHmmss'.0Z'
ldap.synchronization.userIdAttributeName=sAMAccountName
ldap.synchronization.userFirstNameAttributeName=givenName
ldap.synchronization.userLastNameAttributeName=sn
ldap.synchronization.userEmailAttributeName=mail
ldap.synchronization.userOrganizationalIdAttributeName=company
ldap.synchronization.defaultHomeFolderProvider=largeHomeFolderProvider
ldap.synchronization.groupIdAttributeName=cn
ldap.synchronization.groupDisplayNameAttributeName=displayName
ldap.synchronization.groupType=group
ldap.synchronization.personType=user
ldap.synchronization.groupMemberAttributeName=member
ldap.synchronization.enableProgressEstimation=true


Now, when you run the user synchronization, all groups will be imported and all users will show up in their expected groups. Most importantly, you will now only have those users imported from Active Directory who should have access to your Alfresco environment. This will make user management in Alfresco much simpler.

Tuesday, January 23, 2018

How to Change the Alfresco Version Label Format

Document versions in Alfresco are stored as a sequential history of snapshots of the state of the document made over the life of the document.  Each document version is a snapshot of the metadata and file content at the time when the version was created.

Only documents that have the versionable aspect are versioned.  New content uploaded to a document without the versionable aspect will simply be overwritten. In Alfresco Share, by default, all documents are automatically assigned the versionable aspect, so all content will be versioned.

Default version labels in Alfresco are numeric and consist of a major component, followed by the minor component, separated by a period, like "1.0" (major version is 1 and minor version is 0), and "2.5" (major version is 2 and minor version is 5). Also, by default in Share, newly uploaded content will start at version "1.0".

This format ([major].[minor]) for writing and referring to versions is somewhat hardwired into the Alfresco Share interface, but versions don't need to be stored this way in the Alfresco repository.  It is possible to format the version label in any way.

Alfresco provides a "behaviour" called calculateVersionLabel as an extension point where developers are able to wire in a new method for determining the version label.

But note that if you plan to use Alfresco Share as your main interface with the Alfresco repository, it may not be a good idea to change the format for version labels because doing that may cause problems with some assumptions that were made in the Share UI display.  Besides just changing the version label format, there would need to be additional customizations in Share to ensure that the new format for version labels are displayed and handled correctly.

To demonstrate how to change the method for calculating version labels, we will create two new files: a Javascript file that handles the calculation of the version label, and a file to wire in the configuration of this new behaviour.

Let's look first at the Javascript for the behaviour.  Alfresco behaviour code is often written in Java, but here we will just use server-side Javascript.

In this example, we will change the version format so that it still consists of a minor and major component, but instead, will be separated by an underscore.  So, instead of "1.0", the version will be "1_0", for example.

First create the Javascript file: <Tomcat>/shared/classes/alfresco/scripts/onCalculateVersionLabel.js. It contains the following content:

 // Calculate the Version Label  
 // Have a look at the behaviour object that should have been passed  
 if (behaviour == null)   
 {  
   logger.log("The behaviour object has not been set.");  
   scriptFailed = true;  
 }  
 // Check the name of the behaviour  
 if (behaviour.name == null && behaviour.name != "calculateVersionLabel")   
 {  
   logger.log("The behaviour name has not been set correctly.");  
   scriptFailed = true;  
 }   
 else   
 {  
   logger.log("Behaviour name: " + behaviour.name);  
 }  
 // Check the arguments  
 if (behaviour.args == null)   
 {  
   logger.log("The args have not been set.");  
   scriptFailed = true;  
 }   
 else   
 {  
   if (behaviour.args.length == 4)   
   {  
           // Set initial version to 0  
     var currentVersion = (behaviour.args[1]==null)? "0_0" : behaviour.args[1].getVersionLabel();  
           var versionType = behaviour.args[3].versionType;  
     var num = currentVersion.split('_');  
           if(num.length!=2)  
           {  
             scriptFailed = true;  
           }  
           else  
           {  
                var major = parseInt(num[0]);  
                var minor = parseInt(num[1]);  
                if(versionType.toString().equals("MINOR")) currentVersion = major + "_" + (minor + 1);  
                else if(versionType.toString().equals("MAJOR")) currentVersion = (major + 1) + "_0";  
           }  
           currentVersion;  
   }   
   else   
   {  
     logger.log("The number of arguments is incorrect.");  
     scriptFailed = true;  
   }    
 }  

A behaviour object is passed into the Javascript code that contains the parameters of the behaviour.  behaviour.args[1] contains the version object, and behaviour.args[3] is an object with parameters that specify how to construct the new version.

The current version label is stored as behaviour.args[1].getVersionLabel().  The version label will be null for new documents.

The type of versioning, for example, whether to create a new minor or major version, is specified by the value of behaviour.args[3].versionType.

Note that the last line executed in the Javascript code is the return value for the behaviour.  In this case, that value represents the new version label, which is stored in the variable currentVersion.

The Javascript code can then be wired into the behaviour with the following Spring context file.  Place this file in the following location: <Tomcat>/shared/classes/alfresco/extension/version-context.xml.  The configuration calls out the location of the Javascript file to be used for the Alfresco policy calculateVersionLabel.  Two beans are defined in the file.  The first overwrites and undefines the standard method for handling the calculation of version labels, and the second bean defines the new bean for version label calculation.

 <?xml version='1.0' encoding='UTF-8'?>  
 <!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>  
 <beans>  
   <bean id="registerContentWithVersionService" class="org.alfresco.repo.version.VersionServiceVersionLabelRegistrationBean"></bean>  
   <!-- Define a custom behavior on Standard Content -->  
   <bean id="customOnVersionLabelChange" class="org.alfresco.repo.policy.registration.ClassPolicyRegistration" parent="policyRegistration" >  
     <property name="policyName">  
       <value>{http://www.alfresco.org}calculateVersionLabel</value>  
     </property>  
     <property name="className">  
       <value>{http://www.alfresco.org/model/content/1.0}content</value>  
     </property>  
     <property name="behaviour">  
       <bean class="org.alfresco.repo.jscript.ScriptBehaviour" parent="scriptBehaviour">  
         <property name="location">  
           <bean class="org.alfresco.repo.jscript.ClasspathScriptLocation">  
             <constructor-arg>  
               <value>alfresco/scripts/onCalculateVersionLabel.js</value>  
             </constructor-arg>  
           </bean>  
         </property>  
       </bean>  
     </property>  
   </bean>  
 </beans>  

Now, when we create new versions, the new format for the version label will be used. Only newly created versions will use the new method for calculating the version label.

For example, here is the banner on the details page for version 1_3 of a document:








And here is how the version history for that document is displayed on the details page:



There are some problems in Share though.  There are places in Share that assume the version label format.  For example, the following code snippet from the file dnd-upload.js is used when the upload dialog pops up with checkboxes to select the major or minor version.  There is code here that tries to parse the current version label by using the split() method with a "." delimiter to break apart the minor and major components of the label.  This code will have problems when it encounters the new version label in a format like "1_3".



For example, we'll see the following:



Note the "(1.NaN)" which appears for the minor change.  There are likely other problems that will occur within Share because of the change of the version label format too.

But despite those problems, if your application has a need for reformatting version labels, you might consider redefining the method used to calculate the labels.