Working with Transfer ORM: An Event Model Example

2008 January 03
by Paul Marcotte
If you use Transfer ORM you may have seen Mark Mandel's Advanced Transfer ORM Techniques presentation. One of the most interesting portions of this presentation (for me) was his coverage of the Transfer Event Model. Mark demonstrates how to setup a service (in this case a CryptService) as a Transfer Event listener. There are several events that objects can register to as listeners. One event type is "AfterNew", which is fired when a new transfer object is created. In the presentation Mark demonstrates how to leverage the Event Model with an AfterNew event and a decorated transfer object to encrypt/decrypt a user password. I wanted to use this concept for a new project, but rather than copying code verbatim from the presentation, I decided to try something a little different.In the example CryptService, Mark sets the key and algorithm (DESEDE) within the CryptService. I chose to store a unique key for each db record using the GenerateSecretKey() function in ColdFusion and set the encryption algorithm as an constructor argument to the CryptService init() method. Here's a brief breakdown of participants. 1. Transfer ORM 2. A CryptService. 3. A decorator a "user.User" object. 4. ColdSpring (not necessary, just a matter of preference). Here's the ColdSpring bean definition for the CryptService. <bean id="CryptService" class="model.CryptService">
<constructor-arg name="algorithm"><value>DESEDE</value></constructor-arg>
</bean>
Here is the code used to add the CryptService as an event listener (assuming ColdSpring is aliased as "application.beanFactory"). <cfset application.beanFactory.getBean("transfer").addAfterNewObserver(application.beanFactory.getBean("CryptService"))> The important item to note is that any class that you wish to add as an AfterNew observer, must implement a method named actionAfterNewTransferEvent(). The complete code for the CryptService class is below: <cfcomponent displayname="CryptService" output="false" hint="I provide encryption/decryption for strings.">

<cffunction name="init" access="public" output="false" returntype="anthology.model.CryptService">
<cfargument name="algorithm" type="string" required="true">
<cfset setAlgorithm(arguments.algorithm)>
<cfreturn this>
</cffunction>

<cffunction name="setAlgorithm" access="private" returntype="void" output="false">
<cfargument name="algorithm" type="string" required="true">
<cfset variables.algorithm = arguments.algorithm >
</cffunction>

<cffunction name="getAlgorithm" access="public" returntype="string" output="false">
<cfreturn variables.algorithm />
</cffunction>

<cffunction name="encryptString" access="public" output="false" returntype="string">
<cfargument name="stringToEncrypt" type="string" required="true">
<cfargument name="cryptKey" type="string" required="true">
<cfreturn encrypt(arguments.stringToEncrypt,arguments.cryptKey,getAlgorithm())>
</cffunction>

<cffunction name="decryptString" access="public" output="false" returntype="string">
<cfargument name="stringToDecrypt" type="string" required="true">
<cfargument name="cryptKey" type="string" required="true">
<cfreturn decrypt(arguments.stringToDecrypt,arguments.cryptKey,getAlgorithm())>
</cffunction>

<!--- after new transfer event listener method --->
<cffunction name="actionAfterNewTransferEvent" hint="Actions an event before a create happens" access="public" returntype="void" output="false">
<cfargument name="event" hint="The event object" type="transfer.com.events.TransferEvent" required="Yes">
<cfif (arguments.event.getTransferObject().getClassName()) eq "user.User">
<cfset arguments.event.getTransferObject().setCryptService(this)>
</cfif>
</cffunction>

</cfcomponent>
In the actionAfterNewTransferEvent() method above, we test that the class name for the transfer object is "user.User" and invoke the sertCryptService() method passing "this" as the argument. Here are the methods required in my User decorator. <cffunction name="setCryptService" access="public" output="false" returntype="void">
<cfargument name="CryptService" type="model.CryptService" required="true">
<cfset variables.CryptService = arguments.CryptService>
</cffunction>

<cffunction name="getCryptService" access="public" output="false" returntype="model.CryptService">
<cfreturn variables.CryptService>/
</cffunction>

<cffunction name="getPassword" access="public" output="false" returntype="string">
<cfset var local = StructNew()>
<cfset local.password = getTransferObject().getPassword()>
<cfif len(trim(local.password))>
<cfset local.password = getCryptService().decryptString(local.password,getCryptKey())>
</cfif>
<cfreturn local.password>
</cffunction>

<cffunction name="setPassword" access="public" output="false" returntype="void">
<cfargument name="Password" type="string" required="true">
<cfset var local = StructNew()>
<cfset local.password = arguments.Password>
<cfif len(trim(local.password))>
<cfset local.password = getCryptService().encryptString(local.password,getCryptKey()) />
</cfif>
<cfset getTransferObject().setPassword(local.password) />
</cffunction>

<cffunction name="getCryptKey" access="public" output="false" returntype="string">
<cfset var local = StructNew()>
<cfset local.key= getTransferObject().getCryptKey()>
<cfif not len(trim(local.key))>
<cfset local.key = GenerateSecretKey(getCryptService().getAlgorithm())>
<cfset getTransferObject().setCryptKey(local.key)>
</cfif>
<cfreturn local.key>
</cffunction>
In the decorator, the setPassword() and getPassword() methods are where the action takes place. In getPassword() (and getCryptKey()), we check the value of the those methods from the transfer object using getTransferObject() and process the data accordingly. In a nutshell, the password is encrypted when set and decrypted when retrieved. The key used to encrypt/decrypt is generated only if it is not already set (a zero length string). The GenerateSecretKey() function accepts the algorithm name as an argument. For DESEDE GenerateSecretKey() returns a 32 character key. So, for the user table I have a char(32) field where the key is stored. There are obvious pros and cons to storing a unique key for each record. Another thing to consider, is that when a user logs in, you need to retrieve the user by username and compare the decrypted password against the credential provided. This necessitates that each user has a unique username (a not uncommon business rule) which adds a layer of complexity to a user registration. I'll blather more on that subject in another post about transfer objects and validation. This is a pretty long post, so I hope it will be useful to anyone getting started with the Transfer Event Model. If you found this item interesting, I highly recommend watching Mark's excellent presentation for a great introduction from the man himself.