Sunday, January 31. 2010
OpenSSO Reverse Proxy Extension (Part II)
This is the second part of a three post series dedicated to OpenSSO Reverse proxy Extension. In the previous one the code of this extension was checked out and a simple Basic Auth example which is in the samples directory was tested. In this second chapter the reverse proxy is going to be extended in such a way that the software can retrieve the username/password pair from OpenSSO. This is a clear and natural extension in the goal to a robust integration between OpenSSO and the Proxy Extension.
The strategy to obtain that data is the following:
In order to achieve the four items the first step is to install OpenSSO. OpenSSO was installed in the Solaris KVM box and I decided to use the latest build (I wanted to be as close to Express Build 9 as I could). The war installation went perfect following the documentation and, at the end, a working OpenSSO was running inside a container with the user store in a external ldap server.
With the OpenSSO server up and running the next step was the installation of the SDK java example application that goes with OpenSSO bits. The opensso-client-jdk15.war was deployed into my Tomcat container following the installation guide (server runs in the virtualized Solaris and client in my debian laptop). The information I submitted was the following:
The client installation did not go as smooth as the server one. After configuration all Access Management Samples did not work at all. In previous experiences with versions 7.x cookie encoding was specially a headache and I decided to start from here. Finally I found this heaven send web page and activating c66Encode property all the samples magically started to work properly. It was quite useful too enabling debug mode. Single Sign On Token Verification Servlet is the example that lets SDK application to retrieve information from the SSO token.
When the SDK sample app was running the Post Authentication plugin had to be configured. OpenSSO is a very flexible piece of software and post auth classes are useful to execute some code after a successful login. This plugin is a standard OpenSSO post authentication class that inserts into the SSO token the username and password just logged. For example this plugin is used in IIS web agent to replay password against some applications like Sharepoint. The code of this class is in OpenSSO project and the documentation to configure it is also public.
At this point the status of my little OpenSSO installation was the following: OpenSSO was running, the post auth plugin was configured and username/password information was accessible inside token and functional client SDK sample app was running in my tomcat. Basic auth web server was also configured to access the same ldap user store of OpenSSO (this way web server and OpenSSO share the same users). So I just added the OpenSSO SDK in the proxy netbeans project (looking at the sample app) and developed the new password credential source which uses the SDK to retrieve the login information from the token. All this stuff is summarize in:
Although the complete class is presented I want to comment the code briefly. This credential source gets the SSO token. If the token exists and is valid the username must be in the sharepoint_login_attr_value property (if this property is null the usual userId is used) and the password in sunIdentityUserPassword property. But the password is encrypted (Post Auth plugin does) and the method needs to decrypt it using the previously used key. The DES key configured in the plugin must be used and it is passed to the class via the constructor. The main part of the code is the following:
Finally the BasicAuthProxy servlet is changed to use this brand new class. Now it does not use a hardcoded username/password pair but the real one.
If we try to access directly to the tomcat a exception is thrown (cos the user is not logged in OpenSSO). But if we go first to the OpenSSO server, perform a login and then access tomcat, the proxy silently retrieves my login info and logs me in to the basic auth protected web server. The username and password can be shown in the tomcat logs.
As a conclusion, this second part of the proxy extension shows a real OpenSSO use. It is clear that now the extension is not very useful (java developing is needed) but it could be. A third post will come to show a real legacy application instead of the basic auth web server.
cheerio!
The strategy to obtain that data is the following:
- The user must be previously logged in OpenSSO.
- The Post Authentication OpenSSO plugin will be needed to store in the SSO token the username and password (encrypted).
- A new class that implements PasswordCredentialSource is necessary to obtain the data stored in the second point. This class needs the integration of OpenSSO Java SDK into the proxy app.
- The BasicAuthProxy servlet must be slightly modified to integrate the new credential source class.
In order to achieve the four items the first step is to install OpenSSO. OpenSSO was installed in the Solaris KVM box and I decided to use the latest build (I wanted to be as close to Express Build 9 as I could). The war installation went perfect following the documentation and, at the end, a working OpenSSO was running inside a container with the user store in a external ldap server.
With the OpenSSO server up and running the next step was the installation of the SDK java example application that goes with OpenSSO bits. The opensso-client-jdk15.war was deployed into my Tomcat container following the installation guide (server runs in the virtualized Solaris and client in my debian laptop). The information I submitted was the following:
Server Protocol | http |
Server host | solaris10.demo.kvm |
Server post | 8080 |
Server deployment URI | /opensso |
Debug directory | /home/ricky/logs |
Application user name | amadmin |
Application user password | adminadmin |
The client installation did not go as smooth as the server one. After configuration all Access Management Samples did not work at all. In previous experiences with versions 7.x cookie encoding was specially a headache and I decided to start from here. Finally I found this heaven send web page and activating c66Encode property all the samples magically started to work properly. It was quite useful too enabling debug mode. Single Sign On Token Verification Servlet is the example that lets SDK application to retrieve information from the SSO token.
When the SDK sample app was running the Post Authentication plugin had to be configured. OpenSSO is a very flexible piece of software and post auth classes are useful to execute some code after a successful login. This plugin is a standard OpenSSO post authentication class that inserts into the SSO token the username and password just logged. For example this plugin is used in IIS web agent to replay password against some applications like Sharepoint. The code of this class is in OpenSSO project and the documentation to configure it is also public.
At this point the status of my little OpenSSO installation was the following: OpenSSO was running, the post auth plugin was configured and username/password information was accessible inside token and functional client SDK sample app was running in my tomcat. Basic auth web server was also configured to access the same ldap user store of OpenSSO (this way web server and OpenSSO share the same users). So I just added the OpenSSO SDK in the proxy netbeans project (looking at the sample app) and developed the new password credential source which uses the SDK to retrieve the login information from the token. All this stuff is summarize in:
- Adding openssoclientsdk.jar to the project.
- Adding a working AMConfig.properties configuration (copied from the client app) in WEB-INF/classes root directory.
- And finally developing the PostAuthCredentialSource.java class.
Although the complete class is presented I want to comment the code briefly. This credential source gets the SSO token. If the token exists and is valid the username must be in the sharepoint_login_attr_value property (if this property is null the usual userId is used) and the password in sunIdentityUserPassword property. But the password is encrypted (Post Auth plugin does) and the method needs to decrypt it using the previously used key. The DES key configured in the plugin must be used and it is passed to the class via the constructor. The main part of the code is the following:
// Get the SSO token from SSO session
SSOTokenManager manager = SSOTokenManager.getInstance();
SSOToken token = manager.createSSOToken(req);
// check if the token is valid
if (manager.isValidToken(token)) {
// get the user password. PostAuth use sunIdentityUserPassword
// property but this is encrypted using DES. deskeystr has the
// key to decrypt password
String encBase64Password = token.getProperty("sunIdentityUserPassword");
// get the userId. PostAuth use sharepoint_login_attr_value
String userId = token.getProperty("sharepoint_login_attr_value");
if (userId == null) {
// if null use the normal user id
userId = token.getProperty("UserId");
}
// decode password using the key in deskeystr
BASE64Decoder decoder = new BASE64Decoder();
byte[] encPassword = decoder.decodeBuffer(encBase64Password);
byte[] desKey = decoder.decodeBuffer(deskeystr);
SecretKeySpec keySpec = new SecretKeySpec(desKey, "DES");
Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] password = cipher.doFinal(encPassword);
// set user and password in credentials to be used by the proxy
credentials.username = userId;
credentials.password = new String(password).trim();
System.err.println("Username: " + credentials.username);
System.err.println("Password: " + credentials.password);
}
Finally the BasicAuthProxy servlet is changed to use this brand new class. Now it does not use a hardcoded username/password pair but the real one.
public class BasicAuthProxy extends SimpleProxyServlet{
@Override
public void init() throws ServletException {
init("http", "192.168.122.11", 81);
addFilter(new HttpBasicAuthFilter(
new PostAuthCredentialSource("8uDWeq1Mx/c="),
new TemporaryStorage()));
}
}
If we try to access directly to the tomcat a exception is thrown (cos the user is not logged in OpenSSO). But if we go first to the OpenSSO server, perform a login and then access tomcat, the proxy silently retrieves my login info and logs me in to the basic auth protected web server. The username and password can be shown in the tomcat logs.
As a conclusion, this second part of the proxy extension shows a real OpenSSO use. It is clear that now the extension is not very useful (java developing is needed) but it could be. A third post will come to show a real legacy application instead of the basic auth web server.
cheerio!
Saturday, January 30. 2010
OpenSSO Reverse Proxy Extension (Part I)
This time I have been playing with another Sun (AKA Oracle) product: OpenSSO. The Open Web Single Sign-On goal is to provide an extensible foundation for an identity services infrastructure in the public domain, facilitating single sign-on (SSO) for web applications hosted on web and application servers.
OpenSSO is a web framework. Single sign-on can be achieved in any web application with any programming language. Java SDK, J2EE agents, web agents and many other techniques or extensions are available. Nevertheless the applications need to explicitly use the framework and SSO cannot be accomplished in a transparent way. Little modifications are always needed to integrate legacy web applications.
Other SSO software products usually has a password replay feature, which consists in the ability to replay (resend) the password to this legacy applications. Historically this password replay absence has been an OpenSSO weakness against other products like IBM Tivoli Access Manager.
In the roadmap to OpenSSO 8.1 a Reverse Proxy with Password Replay was announced. This title was accompanied with a short description: "Our reverse proxy is being rewritten as a 100% Java proxy that also has the ability to capture and replay passwords for web applications not protected by your single sign-on solution. In short, this will allow Enterprise Single Sign-on (screen scraping) functionality for web applications. Applications that are not protected by OpenSSO can use password replay to do simple password capture and authentication". Theoretically this brand new feature is about to appear in the Express Build 9. As I am a little impatient I was searching throughout the code and I found a new extension called proxy three months ago. These days I have been testing the progress and I am going to present the first of a three posts series about this topic.
The reverse proxy solution is a Java Servlet application which uses Apache HttpCore components to build the necessary HTTP services between client and server. The servlet can have filters to change the information that travels from the client to the server and vice versa. There are filters to manage cookies (CookieFilter), headers (HeaderFilter) or to handle a Basic Authentication (HttpBasicAuthFilter). The other basic idea is a PasswordCredentialSource interface to get the username/password pair someway.
My first goal was to install a web server protected with Basic Authentication and use OpenSSO proxy extension to bypass the login with a static (hardcoded) username and password. All this stuff is a already done sample inside the proxy code (BasicAuthProxy.java).
First of all I checked out the proxy extension directory:
And I reorganized the directories putting all java code (core, samples and contribs) together:
Then I created a new Web Application project in Netbeans using Tomcat 6.0.20 as the web container. The web.xml was copied from the basic auth sample:
And Finally the java servlet was changed to point to my basic auth protected web server (installed in a KVM solaris box):
And it works. If we first access to the web the basic auth challenge pops up. But if we reopen the browser and access directely to the tomcat the web page appears with no login (proxy is silently logging me in).
In summary the proxy extension is clearly in a very first stage, the core is done but there is no integration with OpenSSO. In the next posts I will try to extend proxy with some OpenSSO functionality.
OpenSSO is a web framework. Single sign-on can be achieved in any web application with any programming language. Java SDK, J2EE agents, web agents and many other techniques or extensions are available. Nevertheless the applications need to explicitly use the framework and SSO cannot be accomplished in a transparent way. Little modifications are always needed to integrate legacy web applications.
Other SSO software products usually has a password replay feature, which consists in the ability to replay (resend) the password to this legacy applications. Historically this password replay absence has been an OpenSSO weakness against other products like IBM Tivoli Access Manager.
In the roadmap to OpenSSO 8.1 a Reverse Proxy with Password Replay was announced. This title was accompanied with a short description: "Our reverse proxy is being rewritten as a 100% Java proxy that also has the ability to capture and replay passwords for web applications not protected by your single sign-on solution. In short, this will allow Enterprise Single Sign-on (screen scraping) functionality for web applications. Applications that are not protected by OpenSSO can use password replay to do simple password capture and authentication". Theoretically this brand new feature is about to appear in the Express Build 9. As I am a little impatient I was searching throughout the code and I found a new extension called proxy three months ago. These days I have been testing the progress and I am going to present the first of a three posts series about this topic.
The reverse proxy solution is a Java Servlet application which uses Apache HttpCore components to build the necessary HTTP services between client and server. The servlet can have filters to change the information that travels from the client to the server and vice versa. There are filters to manage cookies (CookieFilter), headers (HeaderFilter) or to handle a Basic Authentication (HttpBasicAuthFilter). The other basic idea is a PasswordCredentialSource interface to get the username/password pair someway.
My first goal was to install a web server protected with Basic Authentication and use OpenSSO proxy extension to bypass the login with a static (hardcoded) username and password. All this stuff is a already done sample inside the proxy code (BasicAuthProxy.java).
First of all I checked out the proxy extension directory:
$ cvs -d :pserver:rickyepoderi@cvs.dev.java.net:/cvs login
$ cvs -d :pserver:rickyepoderi@cvs.dev.java.net:/cvs checkout opensso/extensions/proxy
And I reorganized the directories putting all java code (core, samples and contribs) together:
com/sun/identity/proxy/auth
com/sun/identity/proxy/contrib/sjsme
com/sun/identity/proxy/contrib/pmwiki
com/sun/identity/proxy/contrib/sjsce
com/sun/identity/proxy/contrib/mediawiki
com/sun/identity/proxy/http
com/sun/identity/proxy/util
com/sun/identity/proxy/handler
com/sun/identity/proxy/sample
com/sun/identity/proxy/sample/basicauth
com/sun/identity/proxy/sample/simple
com/sun/identity/proxy/io
com/sun/identity/proxy/servlet
com/sun/identity/proxy/filter
com/sun/identity/proxy/client
Then I created a new Web Application project in Netbeans using Tomcat 6.0.20 as the web container. The web.xml was copied from the basic auth sample:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>Basic Auth Proxy</display-name>
<servlet>
<servlet-name>proxy</servlet-name>
<servlet-class>com.sun.identity.proxy.sample.basicauth.BasicAuthProxy</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>proxy</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
And Finally the java servlet was changed to point to my basic auth protected web server (installed in a KVM solaris box):
public class BasicAuthProxy extends SimpleProxyServlet{
@Override
public void init() throws ServletException {
init("http", "192.168.122.11", 81);
addFilter(new HttpBasicAuthFilter(
new StaticCredentialSource("ricky", "kiosko00"),
new TemporaryStorage()));
}
}
And it works. If we first access to the web the basic auth challenge pops up. But if we reopen the browser and access directely to the tomcat the web page appears with no login (proxy is silently logging me in).
In summary the proxy extension is clearly in a very first stage, the core is done but there is no integration with OpenSSO. In the next posts I will try to extend proxy with some OpenSSO functionality.
Friday, January 22. 2010
Extending IdM SPMLv2 with launchProcess
Service Provisioning Markup Language (SPML) is an XML-based framework, being developed by OASIS, for exchanging user, resource and service provisioning information between cooperating organizations. There are currently two versions of the specification, version 1.0 (SPMLv1) released in 2003 and version 2.0 (SPMLv2) released in 2006. Sun Identity Manager (IdM) is the Sun Identity product and implements SPMLv1 since I know it and SPMLv2 support has been being adopted throughout versions. IdM 7.x and 8.0 versions do not support search (capability to search objects), reference (capability to reference objects from other objects) and updates (a kind of changelog track capability) in their SPMLv2 implementation. The actual 8.1 version released in 2009 added search support for version 2.0. Looking at the client side there is a open source implementation called openspml for both versions in java language.
SPML is usually an important part in any identity deployment with IdM becuase it always covers some of the user cases. In fact this framework is necessary in any provision flow triggered by another application. A white pages application inside the company that let employees to change their passwords or any other data must send changes to the Identity software via SPML. Intranet webapp in which a employee can request a mail or any other role or account for him or other mate has to use again SPML to start a provision flow. Although IdM end user pages can do all this work there are usually previous applications which customers want to not deprecate or a corporate portal they want to use. Here is where SPML rules.
Before IdM 8.1 was released I always used SPMLv1 to integrate this external applications into the new identity solution (the lack of search capability was decisive). But now, with the new 8.1 version, search capability has come into SPMLv2 and a move forward seems necessary. The main risk in the switch is that SPMLv1 has some extended requests: deleteUser, disableUser and enableUser, resetUserPassword and changeUserPassword, launchProcess, listResourcebjects and runForm. Some of them are not covered by any SPMLv2 capability (launchProcess and runForm) and for me launchProcess is a no-go. The SPMLv1 launchProcess extended request is a way to execute any generic IdM workflow and it can resolve any SPML limitation. For example, if the customer wants that managers can approve or reject provision requests using another existing java/web application there is no SPML standard way to do it, and, in this cases, launchProcess goes into action.
Some free time this week let me test this issue and try to extend SPMLv2 IdM implementation with a custom LaunchProcess request. The idea was simple: add SPMLv1 launchProcess into SPMLv2. In this proof of concept (PoC) I just create a new LaunchProcessRequest and LaunchProcessResponse classes in the client part.
This class (with some others) are in the org.openspml.v2.msg.spml package. Previously the name of this package was org.openspml.v2.msg.spmlextended but objects inside this package can't be unmarshalled. The openspml implementation of SPMLv2 uses org.openspml.v2.util.xml.ObjectFactory class to link namespaces in XML with java classes. This class has the mappings hardcoded (maybe I just didn't know how to add a link between urn:oasis:names:tc:SPML:2:0:extended:launchProcessRequest namespace and org.openspml.v2.msg.spmlextended package or simply I forgot something else). Because of that the package was renamed to this new package which is the default for any unknown namespace. This package exists in openspml and it contains classes so my class names were chosen to be different to any standard pre-existing class.
Server implementation consists in a executor class which receives the LaunchProcesRequest object and executes the specified workflow (in the PSO property) with the given input arguments (data extensible DSML attributes). After execution results are set into LaunchRequestResponse object. If the workflow is executed without errors (WavesetResult.hasError method is false) the status is set to SUCCESS and all the data ResultActions are added into data extensible. If the execution has errors, status is FAILURE, error code is set to CUSTOM_ERROR, all error messages are set and data ResultActions added into data in the same way. If some exception is thrown, status is FAILURE, code CUSTOM_ERROR, and exception message is added to error messages (no data is added cos it is not available). This class is LaunchRequestExecutor under com.sun.idm.rpc.spml2.extended package.
Finally a short client code is presented.
The resulting netbeans 6.8 project is here and the steps to run it are the following:
Enjoy it.
SPML is usually an important part in any identity deployment with IdM becuase it always covers some of the user cases. In fact this framework is necessary in any provision flow triggered by another application. A white pages application inside the company that let employees to change their passwords or any other data must send changes to the Identity software via SPML. Intranet webapp in which a employee can request a mail or any other role or account for him or other mate has to use again SPML to start a provision flow. Although IdM end user pages can do all this work there are usually previous applications which customers want to not deprecate or a corporate portal they want to use. Here is where SPML rules.
Before IdM 8.1 was released I always used SPMLv1 to integrate this external applications into the new identity solution (the lack of search capability was decisive). But now, with the new 8.1 version, search capability has come into SPMLv2 and a move forward seems necessary. The main risk in the switch is that SPMLv1 has some extended requests: deleteUser, disableUser and enableUser, resetUserPassword and changeUserPassword, launchProcess, listResourcebjects and runForm. Some of them are not covered by any SPMLv2 capability (launchProcess and runForm) and for me launchProcess is a no-go. The SPMLv1 launchProcess extended request is a way to execute any generic IdM workflow and it can resolve any SPML limitation. For example, if the customer wants that managers can approve or reject provision requests using another existing java/web application there is no SPML standard way to do it, and, in this cases, launchProcess goes into action.
Some free time this week let me test this issue and try to extend SPMLv2 IdM implementation with a custom LaunchProcess request. The idea was simple: add SPMLv1 launchProcess into SPMLv2. In this proof of concept (PoC) I just create a new LaunchProcessRequest and LaunchProcessResponse classes in the client part.
- LaunchProcessRequest. This request has a PSO which represents the workflow to execute and a generic extensible data property. The data is a bunch of DSML attributes that will be passed as inputs to the workflow.
<complexType name="LaunchProcessRequestType">
<complexContent>
<extension base="spml:RequestType">
<sequence>
<element name="psoID" type="spml:PSOIdentifierType" />
<element name="data" type="spml:ExtensibleType" minOccurs="0" />
</sequence>
</extension>
</complexContent>
</complexType>
- LaunchProcessResponse. The launchProcess response consists in the usual outputs (status, error code, error messages,...) and again a extensible data property that has all the outputs of the workflow as DSML attributes.
<complexType name="LaunchProcessResponseType">
<complexContent>
<extension base="spml:ResponseType">
<sequence>
<element name="data" type="spml:ExtensibleType" minOccurs="0" />
</sequence>
</extension>
</complexContent>
</complexType>
This class (with some others) are in the org.openspml.v2.msg.spml package. Previously the name of this package was org.openspml.v2.msg.spmlextended but objects inside this package can't be unmarshalled. The openspml implementation of SPMLv2 uses org.openspml.v2.util.xml.ObjectFactory class to link namespaces in XML with java classes. This class has the mappings hardcoded (maybe I just didn't know how to add a link between urn:oasis:names:tc:SPML:2:0:extended:launchProcessRequest namespace and org.openspml.v2.msg.spmlextended package or simply I forgot something else). Because of that the package was renamed to this new package which is the default for any unknown namespace. This package exists in openspml and it contains classes so my class names were chosen to be different to any standard pre-existing class.
Server implementation consists in a executor class which receives the LaunchProcesRequest object and executes the specified workflow (in the PSO property) with the given input arguments (data extensible DSML attributes). After execution results are set into LaunchRequestResponse object. If the workflow is executed without errors (WavesetResult.hasError method is false) the status is set to SUCCESS and all the data ResultActions are added into data extensible. If the execution has errors, status is FAILURE, error code is set to CUSTOM_ERROR, all error messages are set and data ResultActions added into data in the same way. If some exception is thrown, status is FAILURE, code CUSTOM_ERROR, and exception message is added to error messages (no data is added cos it is not available). This class is LaunchRequestExecutor under com.sun.idm.rpc.spml2.extended package.
Finally a short client code is presented.
SessionAwareSpml2Client client = null;
try {
// config
String url = "http://192.168.122.11:8080/idm/servlet/openspml2";
String user = "configurator";
String password = "configurator";
// start client doing the login
client = new SessionAwareSpml2Client(url);
Response resLogin = client.login(user, password);
// generate a launchprocess request
LaunchProcessRequest req = new LaunchProcessRequest();
req.setExecutionMode(ExecutionMode.SYNCHRONOUS);
req.setPsoID(new PSOIdentifier("Sample Workflow", null, null));
Extensible data = new Extensible();
data.addOpenContentElement(new DSMLAttr("par1", "value1"));
data.addOpenContentElement(new DSMLAttr("par2",
new DSMLValue[] {new DSMLValue("par2-val1"), new DSMLValue("par2-val2")}));
req.setData(data);
Response resLaunch = client.send(req);
} catch (Spml2ExceptionWithResponse e) {
System.err.println("Error inside workflow execution but response given");
Response resLaunch = e.getResponse();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (client != null) {
try {client.logout();} catch(Exception e) {}
}
}
The resulting netbeans 6.8 project is here and the steps to run it are the following:
- Install Sun Identity Manager 8.1 (this is the version I used but newer versions are supposed to work too).
- Add inside the deployed idm.war the launchPrecess.jar of this project (in WEB-INF/lib directory).
- Add the spml2.xml to activate SPMLv2 in IdM. Go to IdM console (http://server:port/idm) and in Configure -> Import Exchange File select the spml2.xml in the extras directory of the project. The spml2.xml has been modified to include the new LaunchProcess capability and its executor (original IdM 8.1 file was backed up in spml2.xml.ORIG in the same directory).
- Import in the same way the two workflow examples in the extras directory:
- Sample Workflow: Just a workflow that returns some data.
- Sample Error Workflow: Workflow which generates error response.
- Open LaunchProcess project with Netbeans.
- Execute the TestLaunchProcess class (you can change the workflow name between "Sample Workflow" and "Sample Error Workflow" in order to execute one workflow or the other).
Enjoy it.
(Page 1 of 1, totaling 3 entries)
Comments