Chapter 17. Administering and Creating Security Providers

Jason Montgomery

No matter how many features a software product has, it always seems to be missing something that would be beneficial or useful in certain situations. K2 blackpearl solves this issue in just about every way by providing a high level of extensibility. K2 blackpearl itself is built on a framework of services that allow for powerful customization, including a Provider model for managing authentication (users who can login) and authorization (roles allowing what users can access). These are called security providers and are also known as User Providers since they expose user information from other systems to the K2 platform. At this time, there is only one functional security provider in K2 blackpearl — the Active Directory User and Role Provider. A SQL Server- based security Provider is currently in the works and should be available around the time this book is published. In most situations, the Active Directory Provider will meet the needs of most organizations. However, situations do arise where having the ability to add a prebuilt custom security provider or even create a custom security provider that ties into third-party systems is desirable. K2 blackpearl utilizes the Provider pattern to allow developers to add their own pluggable custom security providers. Additionally, multiple providers can be used in concert and the K2 system will continue to function as expected allowing for a broad range of interoperability with many different types of systems.

This chapter will explore the administration of the K2 security providers for Active Directory and additionally explore the extensible security provider model for building custom providers to handle custom authentication and authorization within the enterprise. Additionally this chapter will touch on Single Sign-On functionality in K2 and the additional security providers that will be provided by K2 connect.

This chapter covers the following topics:

  • The Active Directory security provider

  • The SQL security provider

  • The security provider API

  • Single Sign-On (SSO)

  • Building a custom Security Provider

  • Installing a custom Security Provider

  • K2 connect

The Active Directory Security Provider

The Active Directory security provider allows K2 blackpearl to integrate seamlessly with Microsoft Active Directory to manage users and roles with K2 blackpearl. Active Directory integration is a great feature because most organizations are already heavily vested in Active Directory. By leveraging the existing Microsoft infrastructure that's already handing authentication and authorization for the domain lowers the overall cost of ownership of K2 blackpearl.

By default, the current domain is detected during K2 blackpearl installation and is the only one used to resolve users and groups; other domains within the forest will not be used. However, K2 can be configured to support multiple domains. The Active Directory User Manager, also known as ADUM, has a group of settings that control how it interacts with Active Directory. This section will cover the configuration settings for ADUM in detail.

ADUM Settings

Most of K2 blackpearl's configuration settings live in the HostServer database in SQL Server. There is a table called SecurityLabels where the ADUM security provider has settings that control certain aspects of how it interacts with Active Directory. These settings can be enumerated by connecting to the K2 blackpearl SQL Server and running the following query. Make sure that you adjust the database name in the following example to match the name given to the HostServer database during K2 blackpearl installation. Take note of the RoleInit column; it contains a semicolon delimited list of the ADUM configuration settings.

USE [HostServer]

SELECT
  SecurityLabelID,
  SecurityLabelName,
  AuthSecurityProviderID,
  AuthInit,
  RoleSecurityProviderID,
  RoleInit,
  DefaultLabel
FROM
  SecurityLabels
WHERE
  SecurityLabelName = 'K2'

An excerpt of the XML data stored in the RoleInit column will look something like this:

<roleprovider>
<init>ADCache=10;LDAPPath=LDAP://DC=DENALLIX,DC=COM;ResolveNestedGroups
=False;IgnoreForeignPrincipals=False;IgnoreUserGroups=False;MultiDomain
=False;DataSources=&lt;DataSources&gt;&lt;DataSource
Path="LDAP://DC=DENALLIX,DC=COM" NetBiosName="DENALLIX"
/&gt;&lt;/DataSources&gt;;;</init>
<login />
. . .
</roleprovidre>

The ADUM initialization settings are listed in the <init> node of the XML document. These settings are described in the following table:

Setting

Definition

Default

ADCache

ADUM maintains a cache of user and group data retrieved from AD. This value controls the number of minutes a value is cached for.

10 min

LDAPPath

The LDAP Distinguished Name (DN) of the domain, for example, LDAP://DC=DENALLIX,DC=COM

The domain DN detected during installation.

ResolveNestedGroups

This setting controls the behavior of how groups referenced within another group are resolved. If this value is false, users within a nested group will not be resolved. If it's set to true, K2 will attempt to resolve all users in all nested groups. If this option is enabled, K2 will keep track of circular group references so as to not get stuck in an infinite cycle.

False

IgnoreForeignPrincipals

A foreign principal is a reference to a user or group in an external domain outside the forest that has a trust relationship established within the forest.

False

IgnoreUserGroups

This setting directs ADUM whether to resolve any groups the user may be a member of. If true, group resolution will not happen for any users. If false, ADUM will resolve the groups the user is a member of.

False

MultiDomain

This setting alerts ADUM that it should handle users and groups from multiple domains within the forest. More on this in the next section.

False

DataSources

This XML document contains data sources for each domain that ADUM will use to resolve users and groups. For example:

<DataSources>
    <DataSource
Path="LDAP://DC=DENALLIX,DC=COM"
              NetBiosName="DENALLIX" />
</DataSources>

Both the Path and NetBiosName are set to Domain values detected during the K2 blackpearl install.

Note

Serious problems with K2 blackpearl can occur if the HostServer database is modified incorrectly. Triple-check configuration changes before applying them and be sure to have a current backup of the database as well as configuration files. Be sure to test the new configuration settings in a development environment before deploying to a production system.

The most common scenario for modifying the configuration settings for ADUM is to add additional support for other domains. The next section will walk you through the process of adding an additional domain that will allow users within that domain to interact and participate in K2 workflows.

Configuring Support for Multiple Domains

Based on the ADUM configuration settings shown above, it's possible to configure K2 blackpearl to use multiple domains within the forest to resolve users and groups. Additionally ADUM can be configured to resolve users and roles from domains outside of the forest that have one- or two-way trusts established.

Two of the configuration settings in the <init> node of the RoleInit column will need to be modified in addition to a couple of changes to the K2 Workspace configuration.

The MultiDomain property documented previously should be set to True. Additionally, a new DataSource node should be added to the DataSources XML document that defines the Path and NetBiosName of the domain being added.

There's another column in the HostServer database that needs to be updated; the AuthInit column contains a list of domains that K2 will authenticate users against.

The following is an example of a SQL Script that will update all the appropriate fields to add multi-domain support for the DENALLIX domain as well as the CENTRAL-DLX domain. Make sure to extract the current settings from your environment, and then merge in the appropriate changes noted in bold/italics in the following example.

USE [HostServer]

UPDATE
  [SecurityLabels]
SET
  AuthInit =
'<AuthInit>
  <Domain>DENALLIX</Domain>
<Domain>CENTRAL-DLX</Domain>
</AuthInit>',
  Roleinit =
'<roleprovider>
<init>ADCache=10;LDAPPath=LDAP://DC=DENALLIX,DC=COM;ResolveNestedGroups
=False;IgnoreForeignPrincipals=False;IgnoreUserGroups=False;MultiDomain
=True; DataSources=<DataSources>&lt;DataSource Path="LDAP://DC=DENALLIX,DC=COM"
NetBiosName="DENALLIX" /><DataSource Path="LDAP://DC=CENTRAL-DLX,
DC=DENALLIX,DC=COM"
NetBiosName="CENTRAL-DLX" />&lt;/DataSources&gt;;;</init>
    <login />
   <implementation assembly="ADUM, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=16a2c5aaaa1b130d" type="ADUM.K2UserManager2" />
  <properties>
    <user>
      <property name="Name" type="System.String" />
      <property name="Description" type="System.String" />
      <property name="Email" type="System.String" />
      <property name="Manager" type="System.String" />
      <property name="SipAccount" type="System.String" />
      <property name="ObjectSID" type="System.String" />
   </user>
   <group>
    <property name="Name" type="System.String" />
    <property name="Description" type="System.String" />
   </group>
  </properties>
</roleprovider>'
WHERE
  SecurityLabelName = 'K2'

After the SecurityLabels table in the HostServer database has been updated, there is one final area requiring configuration — the K2 Workspace web.config will need updated to take into account the new domain. This file is located in the [K2 Install Folder]WorkspaceSite folder.

In the connection string settings of the web.config file, add the following to the connectionString section, making the appropriate adjustments for your environment:

<add name="CNTRLDLX-ADConnString" connectionString="LDAP://CENTRAL-DLX.com" />

Finally, in the membership section, add a new provider that uses the connection string added in the previous step, as follows — take particular node of the bold/italics text in the example. The connectionString name must match the connection string from the previous step, and the name attribute must also be unique.

<add connectionStringName="CNTRLDLX-ADConnString
     connectionProtection="Secure"
     enablePasswordReset="false"
     enableSearchMethods="true"
     requiresQuestionAndAnswer="false"
     applicationName="/"
     description="Central DLX AD connection"
     requiresUniqueEmail="false"
     clientSearchTimeout="30"
     serverSearchTimeout="30"
     attributeMapUsername="sAMAccountName"
     name="AspNetActiveDirectoryMembershipProvider_CentralDlx"
     type="System.Web.Security.ActiveDirectoryMembershipProvider,System.Web,
     Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />

This process can be repeated to add support for as many domains in K2 as is needed.

The SQL Security Provider

Having Active Directory integration, as discussed in the previous section, is a no-brainer; however, not every user store within the enterprise lives in Active Directory nor can they all be. To help solve this issue, the SQL Role Provider will be a SQL Server-based User and Role Provider, allowing the full administration of the SQL Users and Roles within the K2 Workspace. Users who are outside the scope of the enterprise's Active Directory will be able to have accounts within K2 blackpearl, participate in workflows, and have access to the K2 Workspace using the SQL Role Provider. All the user and role information will be stored in a SQL database. And for those organizations that would rather not have K2 tie into Active Directory at all, the SQL security provider will be the primary security provider, and Active Directory integration will be optional.

As it stands now, the SQL Role Provider is on track to be included with K2 blackpearl SP2, which should be available around the time this book is published.

The Security Provider API

Not every organization uses Active Directory and not every organization uses SQL Server, so requiring one or the other may be overly restrictive. That leaves a third option — rolling your own security provider to use the existing authentication and authorization mechanisms within your enterprise. K2 blackpearl provides a pluggable architecture that can be added to or replaces the current Authentication and Authorization Providers. Or perhaps your organization uses Active Directory and can rely on ADUM, but there are business requirements that require you to allow business partners to have access to K2 — adding an additional security provider in these situations makes a lot of sense. K2 workflow processes allow the mixing and matching of users from multiple security providers so that you can get the most out of the workflow engine.

This section covers the K2 blackpearl APIs needed for building these pluggable security providers.

The Security Provider Object Model

The K2 blackpearl Host Server, the core server that hosts all of the K2 Services, has an extensible architecture that allows pluggable components. In order for an object to be loaded by the Host Server and also be recognized and used as a security provider, it must implement the IHostableSecurityProvider interface. This interface is built on three other interfaces, the IHostableType interface, the IAuthenticationProvider interface, and the IRoleProvider interface.

All pluggable components that are loaded by the Host Server must implement the IHostableType interface. The Host Server will be able to dynamically load, initialize, and unload objects that implement the IHostableType interface.

Additionally, all K2 security providers require two interfaces to function — one for handling authentication via the IAuthenticationProvider and another for handling User and Role functionality through the IRoleProvider interface. When implementing this interface, additional interfaces need to be implemented that represent the Users and Groups for the custom security provider — more on these later.

These three interfaces, IHostableType, IAuthenticationProvider, and IRoleProvider are all inherited by the IHostableSecurityProvider interface. This main interface, IHostableSecurityProvider, must be implemented in order to create a custom security provider, so it will be important to have an understanding of each interface's function and their relationships. Take a moment to study the following Class Diagram shown in Figure 17-1 to understand these relationships.

Figure 17-1

Figure 17.1. Figure 17-1

Notice that there is some overlap in the method interface names, like the Init() and Login() methods. This may cause some initial confusion, but there is good reason for the duplication of functions on these multiple interfaces. For example, three different Init() methods are required — one for each interface. This is because the IAuthenticationProvider and the IRoleProvider are instantiated separately.

The IAuthenticationProvider and IRoleProvider are instantiated separately by the Host Server, meaning that while they all share the same class definition, they are initialized as separate object instances in memory. That's why each interface has an Init() method defined. If there are similar initialization requirements between the IAuthenticationProvider and IRoleProvider, the code must be repeated in each Init() method. However, any class level objects initialized and assigned in the IHostableType.Init() method are available to the IRoleProvider or IAuthenticationProvider.

Even though the IAuthenticationProvider and the IRoleProvider are combined into the same final IHostableSecurityProvider interface, ultimately both interfaces do not have to be implemented. Additionally providers can be mixed and matched. Based on the way the security provider configuration is handled and also how each provider is instantiated separately gives them a bit of flexibility allowing them to be completely decoupled. So, for example, if building an IAuthenticationProvider, none of the IRoleProvider interface methods have to be implemented; they could simply throw a NotImplementedException exception. This is also why the Login() method is defined on both the IRoleProvider and the IAuthenticationProvider interfaces. Since each provider can be decoupled, they may use completely different mechanisms to log in to retrieve groups or authenticate users. It's also possible that only one provider may be needed; for example, in certain situations, only the IAuthenticationProvider may be employed without the need for an IRoleProvider.

A Pluggable Architecture

With the object model defined, the Host Server must have a way to dynamically load and initialize security providers. It employs a plugin architecture based on the Interfaces defined in the object model, combined with the configuration settings in the HostServer database. The actual installation of a security provider is covered in great detail later in this chapter; this section will attempt to map out its architecture.

There is one initial event that triggers a security provider to be loaded by the Host Server. By dropping an assembly containing a specific object type into the securityproviders folder on the K2 server, the next time the K2 Service starts up, it polls this folder for assemblies containing an IHostableSecurityProvider type. Once this file containing this type is loaded by the Host Server, it then records the new security provider by inserting a row into the database. The next and final step is manual — the database needs to be updated to give the security provider a friendly name, called a security label, and to also provide configuration data for the IAuthenticationProvider and the IRoleProvider.

The two tables in the HostServer database that are used to construct a security provider are the SecurityLabels and SecurityProvider tables shown in Figure 17-2. The SecurityLabels table stores a record for each security provider.

Figure 17-2

Figure 17.2. Figure 17-2

Based on the table relationships defined in Figure 17-2, a security provider is defined as a unique security label with one Authentication security provider Class and one Role security provider Class, and the corresponding Initialization data for each.

The first field is the SecurityLabelName. This field stores the security label, which is the friendly name used by K2 blackpearl to identify a security provider. For instance, K2 is the security label for the Active Directory provider, and K2SQL is used for the K2 SQL Role Manager. So for K2 to identify a user, it will combine the security label with the username, delimited with a colon. For example, for K2 to identify the Administrator account in the Active Directory Domain called DENALLIX using ADUM, the username would be represented as K2:DENALLIXAdministrator.

Next is the AuthSecurityProviderID column. This is a Foreign Key to the SecurityProviderID in the SecurityProvider table. This table maps a unique security label to the security provider's Class name. For instance, the security label K2 in the SecurityLabels table relates to a provider class name of SourceCode.Hosting.SecurityProviders.SSPI in the SecurityProvider table.

The AuthInit field coupled with the AuthSecurityProviderID field and its relationship to the SecurityProvider table provides the Host Server with the information needed to initialize the IAuthenticationProvider when the Host Server starts up. The AuthInit field contains an XML document containing configuration data used to initialize the IAuthenticationProvider. In the case of SourceCode.Hosting.SecurityProviders.SSPI, the XML document stored in the AuthInit XML column contains the following.

<AuthInit>
  <Domain>DENALLIX</Domain>
</AuthInit>

Likewise, the RoleInit column coupled with the RoleSecurityProviderID and the related SecurityProvider table provides the Host Server with the configuration information needed to initialize the IRoleProvider. An example of the RoleInit column data was shown earlier in this chapter when covering how to change the ADUM's Active Directory configuration settings.

With all these pieces put together, when K2 encounters a security label, it looks up the corresponding security provider and passes off control to the security provider based on a coding contract — which is the security provider's Interfaces.

IHostableType

The IHostableType interface is used during the startup and shutdown of the Host Server. This interface is straightforward. When the Host Server starts, it calls the IHostableType.Init() method on all the installed security providers, and likewise when the Host Server is shutting down, it calls IHostableType.Unload() on all installed security providers so they can clean up any resources that need to be freed.

The following table documents the interface members.

Methods

Description

Init

Initializes the IHostableType. This is called when the Host Server Starts up.

Unload

Unloads the Provider. This is called when the Host Server shuts down.

The IHostableType.Init() method is used to pass in context objects from the Host Server, so the IHostableType being implemented can access objects like the Configuration Manager or Security Manager or any other server hosted in the Host Server. Additionally, the Host Server context objects IServiceMarshalling and IServerMarshalling which are passed into the IHostableType.Init() method also contain methods for marshalling data between the custom security provider and the Host Server.

The following code snippet shows how to set up a reference to the Configuration Manager, Security Manager, and references to other servers hosted in the Host Server — in this example, the Logger. These items are covered in more detail in the next section.

using SourceCode.Hosting.Client.BaseAPI;
using SourceCode.Hosting.Server.Interfaces;

public class CustomSecurityProvider : IHostableSecurityProvider
{
 private Logger _logger;
 private IConfigurationManager _configurationManager;
 private ISecurityManager _securityManager;

 #region IHostableType Members

 public void Init(IServiceMarshalling ServiceMarshalling,
                             IServerMarshaling ServerMarshaling)
 {
  // Get a reference to the Configuration Manager Object
  _configurationManager = serviceMarshalling.GetConfigurationManagerContext();

  // Get a reference to the Security Manager Object
  _securityManager = ServerMarshaling.GetSecurityManagerContext();

   // Get a reference to the Logger Object
  _logger = (Logger)ServiceMarshalling.GetHostedService(typeof(Logger).ToString());
 }

 public void Unload()
 {
  // Perform any clean-up required when the service shuts down
 }
  #endregion
. . .
}

Notice how the IServiceMarshalling and IServerMarshaling classes provide access to objects in the Host Server in the Init() method. In many cases, the Unload method may not have any code; it's there to provide the ability to properly handle the freeing or closing of managed and/or unmanaged resources when needed.

It's All About Context

A security provider doesn't exist in a vacuum; it needs contextual access to functionality provided by objects in the Host Server. The objects of use when developing a custom security provider are the Configuration Manager, the Security Manager, and the Logger.

The Configuration Manager

The Configuration Manager provides access to server configuration settings such as licensing, current users, the path to the Host Server configuration file to name a few. For most custom security providers, the K2 Configuration Manager won't be of much use. The following code shows how to get a reference to the Configuration Manager through the serviceMarshalling argument of the Init() method.

// Get a reference to the Configuration Manager Object
_configurationManager = serviceMarshalling.GetConfigurationManagerContext();

The Host Server's Configuration file, located in [K2 Install]Host ServerinK2HostServer.config, can be used to store configuration values using .NET's built-in Configuration framework. For example, to add a connection string that's available to a custom security provider, the following connection string could be added to the K2HostServer.config file:

<add name="CustomProviderName.Connectionstring" connectionString="Data
Source=DLX;Database=CustomProviderDB; Integrated Security=True" />

To retrieve values stored in the Host Server's configuration file, use .NET's Configuration Manger class (System.Configuration.ConfigurationManager), like this:

string conn =
ConfigurationManager.ConnectionStrings["CustomProviderName.Connectionst
ring"].ConnectionString;

What's nice about using the Host Server's configuration file to store connection strings is that K2 will automatically encrypt the <connectionStrings> section, protecting any usernames and passwords that exist in that part of the configuration file using the Protected Configuration mechanisms provided in .NET 2.0.

The Security Manager

The Security Manager provides access to different security features of the Host Server, including getting a reference to an authentication provider, access to cryptographic features of the Host Server, and the ability to enumerate the different security and role providers loaded in the Host Server. The following code demonstrates how to get a reference to the Security Manager:

// Get a reference to the Security Manager Object
_securityManager = ServerMarshaling.GetSecurityManagerContext();

When implementing a custom security provider, the ISecurityManager.GetSecurityLabelItem() method can be used to retrieve the initialization data for the custom security provider. This method allows a custom security provider to retrieve the XML-formatted custom configuration settings from the Host Server database.

string label = "SECURITY_LABEL";

// Retrieve the Initialization data from the Host Server
// database.
string initData = _securityManager.GetSecurityLabelItem("AuthInit", label) as string;

// Parse the initData to initialize the provider
. . .

The previous example shows how to retrieve the XML initialization data stored in the AuthInit column in the SecurityLabels table in the HostServer database where SecurityLabelName is equal to SECURITY_LABEL. The security label is the friendly name used to uniquely identify any security provider in K2.

In the introduction to the security provider API earlier in the chapter, it was pointed out that the IRoleProvider and the ISecurityProvider are separate object instances in memory, which means that fields initialized by one aren't accessible to the other, thus the need for multiple Init() methods. However, the Security Manager provides the ability for one provider to access another, and additionally the Security Manager can also provide a reference to any other security provider in the system, given the corresponding security label. The following example shows how to access another security provider running in the Host Server.

// Get a reference to the IRoleProvider for the given _securityLabel
IRoleProvider roleProvider =
           _securityManager.GetRoleProviderContext(_securityLabel);

// Get a reference to the IAuthenticationProvider for the given _securityLabel
IAuthenticationProvider authProvider =
            _securityManager.GetAuthenticationProviderContext(_securityLabel);

The Logger

Another important item for use when developing a custom security provider is SourceCode.Logging.Logger. A reference to the logger allows security providers access to the main Host Server logging mechanism, allowing log messages to be written to the Debug Console, Host Server Log File, System Event log, and so on.

See Chapter 19 on logging and system reporting for more information on the SourceCode.Logging.Logger namespace.

The following code shows how to get a reference to the Logger:

// Get a reference to the Logger Object
_logger = (Logger) ServiceMarshalling.GetHostedService(typeof(Logger).ToString());

The following table lists the logging methods of interest when creating a custom security provider:

Methods

Description

LogDebugMessage

Write a message Debug Severity to the Logger. Use this for debugging purposes.

LogErrorMessage

Write a message with Error Severity to the Logger. Code that detects errors that are NOT in a try. . .catch block should use this method.

LogExceptionMsg

Writes an Exception to the Logger. Use this method to log exceptions in a try. . .catch block.

LogInfoMessage

Write an Informational message to the Logger. This is used for writing noteworthy information to the logger.

LogWarningMessage

Writes a message with the Warning Severity to the Logger. Used to write warnings to the Logger when encountering certain code conditions that may cause unknown or undesirable behavior.

The Host Server allows for different levels of logging based on the Logger's Severity configuration set in the [K2 Install Path]Host ServerinHostServerLogging.config file. When developing a custom security provider, make sure to enable the Debug Severity to the appropriate logging mechanism. This will allow for any debugging message output from the custom security provider being developed to be logged.

IHostableSecurityProvider

The IHostableSecurityProvider brings an addition of one method called RequiresAuthentication. It turns out that the developer need not do much more then always return true.

Methods

Description

RequiresAuthentication

This method is a leftover artifact and should always return true. It may eventually be removed from the Interface API.

IAuthenticationProvider

The IAuthenticationProvider interface is where the authentication work is done through the AuthenticateUser method. Additionally, the IAuthenticationProvider.Init() method serves an important role as it is how the Authentication provider gets the initialization data from the HostServer database.

The following table documents the IAuthenticationProvider interface:

Methods

Description

AuthenticateUser

Used to authenticate the user.

Init

Initializes the IAuthenticationProvider.

Login

Used to log in to the underlying security provider store using the provided connection string. Once the Login succeeds, the open connection can be used for authenticating the user if necessary. This method is invoked when the Workflow client opens a connection to the K2 server using this security providers' security label.

IAuthenticationProvider Initialization

When K2 instantiates the IAuthenticationProvider, it invokes IAuthenticationProvider.Init(string label, string authInit). The string label contains the security label name and the authInit data, which contains the name of the AuthInit column in the SecurityLabels table. For the following example, assume the AuthInit column in the database contains the XML Document from the DENALLIX domain. The code to initialize a security provider might look something like this:

void IAuthenticationProvider.Init(string label, string authInit)
{
  // Retrieve the AuthInit data from the database
  string authInit =
     securityManager.GetSecurityLabelItem("AuthInit", label) as string;

  // Make sure authInit has a value
  if (!string.IsNullOrEmpty(authInit))
  {
    // Retrieve configuration settings from the
    // AuthInit Xml document
    XPathDocument document = new
            XPathDocument(XmlReader.Create(new StringReader(authInit)));

    // Select the Active Directory Domain
    string domain = document.SelectSingleNode("/AuthInit/Domain").InnerText;
    . . .
   }
. . .
}

Use the AuthInit field in the database to store configuration settings or any dynamic settings that should not be hardcoded in the security provider assembly. This will allow configuration changes later without recompiling a new version of the provider.

Authenticating Users

When the Host Server receives a request for a user to log in/authenticate, the Host Server will call the IAuthenticationProvider.AuthenticateUser method on the security provider for the given security label.

The interface defines IAuthenticationProvider.AuthenticateUser() as follows:

public bool AuthenticateUser(string userName, string password, string extraData);

The following table defines each argument:

Method Argument

Definition

userName

The name of the user to authenticate.

password

The user's password.

extraData

This field can contain additional metadata that originates from the client's connection string and is passed through to the security provider on the server. The value of the AuthData field in the client's connection string will be assigned to this argument. In many cases it won't be used unless the custom security provider takes the code and explicitly does something with it.

Returns

A Boolean value — True if the user is successfully authenticated, false if the user does not succeed.

The following example creates a connection string and logs in to the K2 server using the Client API:

SCConnectionStringBuilder builder = new SCConnectionStringBuilder();
        builder.Host = "DLX";
        builder.Port = 5252;
        builder.SecurityLabelName = "CUSTOM_K2";
        builder.UserID = @"jmontgomery";
        builder.Password = "th1s1smyp4ssw0rdw00t!@";
        builder.Integrated = false;
        builder.IsPrimaryLogin = true;
        builder.AuthData = "extra config metadata";

// Login to the K2 Server
Connection k2Conn = new Connection();
k2Conn.Open("DLX", builder.ToString());

The Host Server will receive the request to authenticate the user from the client and will look up the appropriate security provider based on the provided security label name "CUSTOM_K2" set in the connection string. The Host Server will then call the IAuthenticationProvider.AuthenticateUser() method, passing in the following arguments:

Username = "jmontgomery"
password = "th1s1smyp4ssw0rdw00t!@"
extraData = "extra config metadata"

If the credentials are correct, AuthenticateUser() will return true, notifying the Host Server that the user was successfully authenticated. If false is returned, the Host Server will throw an exception back to the client and they will be notified of an unsuccessful login.

IRoleProvider

The IRoleProvider handles all user and role based functions for the security provider. The name IRoleProvider may initially cause confusion because of the word "Role" in the name. It not only handles finding roles/groups, it also handles all the user functionality. The following table documents the members on this interface, and each will be explored further as the chapter continues:

Methods

Description

Init

Initializes the provider.

Login

This method performs a login to the underlying Role provider data store using the given connection string. Once the login succeeds, the open connection can then be used to retrieve Groups and Users from the underlying Role provider. This method is invoked when opening a connection from a client workflow when using this provider's security label to connect.

GetUser

Gets an IUser by username.

FindUsers

Searches the underlying security provider data store for users who are members of groupName, or that match the given search criteria in the properties dictionary. Returns an IUserCollection of matches.

QueryUserProperties

Returns a dictionary that contains all defined IUser.Properties valid for the security provider's IUser object and the corresponding types.

GetGroup

Returns an IGroup object by name.

FindGroups

Searches the underlying security provider data store for groups that the userName is a member of, or that match the given criteria specified in the properties dictionary. Returns an IGroupCollection of the matches.

QueryGroupProperties

Returns a dictionary containing all defined Properties valid for the security provider's IGroup object and the corresponding types.

FormatItemName

Formats usernames in a consistent format. This method is used to normalize all usernames for a provider so that they are consistent. Domain names can be represented user@domain or domainuser. This is a convenience method added for developers to reference within their own classes. The K2 server itself doesn't appear to ever call this method.

ResolveQueue

Resolves all users and groups in a destination queue that are listed in the provided XML document.

Initializing the Role Provider

The IRoleProvider's initialization works very similarly to the IAuthenticationProvider initialization. The difference is that the IRoleProvider.Init() method is passed the RoleInit column name instead as the value for the roleInit parameter.

The following snippet is an example of an IRoleProvider.Init() method:

void IRoleProvider.Init(string label, string roleInit)
{
  // Retrieve the RoleInit data from the database
  roleInit =
     this._securityManager.GetSecurityLabelItem("RoleInit", label) as string;

  // Make sure roleInit has a value
  if (!string.IsNullOrEmpty(roleInit))
  {
     // extract Role Providers from Xml document
     . . .
  }
. . .
}

Once the values are extracted from the RoleInit XML document, they can be extracted and stored in field variables on the IHostableSecurityProvider object so that they can be used throughout the class.

Before exploring the specifics of some of the IRoleProvider methods, such as IRoleProvider.FindUsers() and IRoleProvider.FindGroups(), types for Users and Groups must be understood. The SourceCode.Hosting.Server.Interfaces namespace has four additional interfaces needed for building custom User and Role objects that work within K2 blackpearl. These interfaces are IUser, IUserCollection, IGroup, and IGroupCollection.

IUser and IUserCollection Interfaces

Figure 17-3 shows the class diagram for the IUser and IUserCollection. The interface defines the UserID and the UserName for the user within K2 blackpearl. The IUser.Properties dictionary can contain any attributes deemed useful, for example, phone number, address, cell phone, and so on.

Figure 17-3

Figure 17.3. Figure 17-3

The IRoleProvider.FindUsers() and IRoleProvider.GetUser() methods will return the IUser and IUserCollection objects. The following table defines the different IUser properties:

Property

Definition

UserID

The User ID is used to uniquely identify a user within K2. It combines the security label and the security provider's username separated by a colon ':' — for example, K2:DENALLIXRobert.

UserName

The username field contains the security provider's username — for example, DENALLIXRobert.

Properties

A dictionary of attributes associated with the user.

In order for an IUser object to function properly in the K2 environment, there are several required properties that MUST be added to the IUser.Properties collection. They are listed and defined in the following table:

Required Properties

Definition

Name

The user's name, for example, Robert Smith.

Description

A description associated with the user.

Email

The user's e-mail address for use in notification.

Manager

The user's manager — this is used in K2 processes for resolving a user's manager for approval, routing, escalation, and so on.

Additionally, any number of custom properties can be added to provide additional user data to K2 Processes. For example, K2 allows support for instant messaging and the IUser.Properties dictionary contains an attribute called SipAccount to facilitate that functionality.

Here is a sample implementation of the IUser interface, with only the required properties:

class CustomUser : IUser
{
    private IDictionary<String, Object> _properties;
    private string _userID;
    private string _userName;

    #region IUser Members

    public IDictionary<string, object> Properties
    {
        get { return _properties; }
        set { _properties = value; }
    }

    public string UserID
    {
        get { return _userID; }
        set { _userID = value; }
    }

    public string UserName
    {
        get { return _userName; }
        set { _userName = value; }
    }

    #endregion

    public CustomUser(
        string securityLabel,
        string userName,
        string name,
        string description,
        string email,
        string manager)
    {
        _userID = securityLabel + ":" + userName;
        _userName = userName;

        _properties = new Dictionary<string, object>();
        _properties.Add("Name", name);
        _properties.Add("Description", description);
        _properties.Add("Email", email);
        _properties.Add("Manager", manager);
    }
}

Notice that the UserID is made up of a combination of the security label and the username. This guarantees that the UserID for a given provider is unique within the system. Also, the name, description, email, and manager properties are the minimum fields required to be added to an IUser to properly function within K2. Additional fields can be added to add support for additional functionality in your workflows.

Note

At the time of writing, the following characters should not be used anywhere in the UserName field due to character filtering in the K2 Workspace. Using any of these characters will cause errors and failures when using a custom user provider within the K2 Workspace.

Excluded Special Characters:

&!@#$%^&*()+=-[]';,./{}|":<>?_~

Always excluded:

-:.&$#@

This may change in later releases. For current information on filtered characters and for a full list of filtered Chinese characters, please see Knowledge Base Article KB000299 on the K2 Web site.

The other class required is IUserCollection.

class CustomUserCollection : IUserCollection
{
    private Collection<IUser> _collection;

    public CustomUserCollection()
    {
        _collection = new Collection<IUser>();
    }

    #region IUserCollection Members

    public IUser this[int index]
    {
        get { return _collection[index]; }
    }

    #endregion

    #region IEnumerable Members

    public System.Collections.IEnumerator GetEnumerator()
    {
        return _collection.GetEnumerator();
    }

    #endregion

    public void Add(IUser user)
    {
        _collection.Add(user);
    }
}

That's as simple as it needs to be. More functionality can certainly be added, but the two classes stubbed out as shown in the preceding code would be enough to build a complete provider. Next, we explore how to retrieve users and groups.

IGroup and IGroupCollection Interfaces

Figure 17-4 shows the class diagram for the IGroup and IGroupCollection. This interface defines the GroupID and GroupName properties as well as a property collection called Properties used for any additional fields that will be made available to the K2 Processes.

Figure 17-4

Figure 17.4. Figure 17-4

The following table documents the class members of the IGroup interface:

Property

Definition

GroupID

The Group ID is used to uniquely identify a group within K2. It combines the security label and the security provider's group name separated by a colon ':' — for example, K2:DENALLIXITUsers.

GroupName

The username field contains the security provider's group name — for example, DENALLIXITUsers.

Properties

A dictionary of attributes associated with the group.

The IRoleProvider.FindGroups() and IRoleProvider.GetGroup() methods return IGroup and IGroupCollection objects.

In order for an IGroup object to be compatible with K2, there are two required properties that MUST be added to the IGroup.Properties collection. They are listed and defined in the following table:

Required Properties

Definition

Name

The group name

Description

A description for the group

Additional custom fields can be added to the IGroup as well to facilitate additional functionality in K2 processes.

The actual implementation of the IGroup and IGroupCollection is almost identical to the IUser and IUserCollection implementations. The following is an implementation of the IGroup interface:

class CustomGroup : SourceCode.Hosting.Server.Interfaces.IGroup
{
    private string _groupID;
    private string _groupName;
    private IDictionary<string, object> _properties;

    #region IGroup Members

    public string GroupID
    {
        get { return _groupID; }
        set { _groupID = value; }
    }

    public string GroupName
    {
        get { return _groupName; }
        set { _groupName = value; }
    }

    public IDictionary<string, object> Properties
    {
        get { return _properties; }
        set { _properties = value; }
    }
    #endregion

    public CustomGroup(string securityLabel, string groupName, string description)
    {
        _groupID = securityLabel + ":" + groupName;
        _groupName = groupName;

        _properties = new Dictionary<string, object>();
        _properties.Add("Name", _groupID);
        _properties.Add("Description", description);
    }
}

Notice that the GroupID combines the group's name with the security label. This makes the GroupID unique in K2 for any security provider. Also, remember the Name and Description properties are required for any IGroup implementation. Additional properties can be added to add additional functionality into K2 blackpearl.

The following code is a sample implementation of the IGroupCollection interface:

class CustomGroupCollection : SourceCode.Hosting.Server.Interfaces.IGroupCollection
{
    Collection<IGroup> _groups;

    #region IGroupCollection Members

    public SourceCode.Hosting.Server.Interfaces.IGroup this[int index]
    {
        get { return _groups[index]; }
    }

    #endregion

    #region IEnumerable Members

    public System.Collections.IEnumerator GetEnumerator()
    {
        return _groups.GetEnumerator();
    }

    #endregion

    public CustomGroupCollection()
    {
        _groups = new Collection<IGroup>();
    }

    public void Add(IGroup group)
    {
        _groups.Add(group);
    }
}

These sample implementations of the IGroup and IGroupCollection interfaces contain all the functionality needed to use them in an actual security provider.

Retrieving Users and Groups

There are two methods defined on the interface that retrieve one user or one group, each with one overload for extra metadata:

  • IRoleProvider.GetUser()

  • IRoleProvider.GetRole()

Each is defined as follows:

// Interface Method Definitions for GetUser
IUser GetUser(string name);
IUser GetUser(string name, string extraData);

// Interface Method Definitions for GetGroup()
IGroup GetGroup(string name);
IGroup GetGroup(string name, string extraData);

The following table defines both FindUser and FindGroup:

Argument

Definition

Name

The exact name of the user or group to retrieve.

extraData

Any extra metadata.

Returns

Returns one IUser or IGroup that corresponds to the name that was passed in.

IRoleProvider.GetUser() is called, for example, immediately after an AuthenticateUser() method is run to retrieve user information.

Searching for Users and Groups

In order to search the underlying Role provider store for users and groups, a dictionary of properties is provided to the Find methods with the corresponding search criteria. In order to be able to implement the search properly, K2 needs to know what properties (required and custom) have been defined for the provider.

K2 is able to discover what properties and their corresponding types are defined on the IUser and IGroup objects of a security provider by calling the IRoleProvider.QueryUserProperties() and IRoleProvider.QueryGroupProperties() methods. Before every search, K2 queries the properties and then fills in the search criteria to the appropriate ones.

The basic implementation of the QueryUserProperties() method, taking into account the required properties, would look like this:

public Dictionary<string, string> QueryUserProperties()
{
    Dictionary<string,string> userProperties = new Dictionary<string, string>();
    userProperties.Add("Name", "System.String");
    userProperties.Add("Description", "System.String");
    userProperties.Add("Email", "System.String");
    userProperties.Add("Manager", "System.String");
    return _userProperties;
}

A more flexible option, other then hardcoding the list into the Role provider, might be to store a list of the properties in the RoleInit XML document so that the list could be changed without recompiling. This is exactly how the ADUM security provider is configured.

Similarly, the most basic implementation of the QueryGroupProperties() method would look like this:

public Dictionary<string, string> QueryGroupProperties()
{
    Dictionary<string,string> groupProperties = new Dictionary<string, string>();
    groupProperties.Add("Name", "System.String");
    groupProperties.Add("Description", "System.String");
    return groupProperties;
}

Now that K2 knows what properties the custom IUser and IGroup objects contain, it can now successfully search for them.

Searching for Users

The method IRoleProvider.FindUser() retrieves a collection of users from the security provider's data store — SQL Server, LDAP, or so forth. There are two interface signatures; one requires two arguments, and the other requires three arguments:

  • groupName

  • properties

  • extraData

IUserCollection IRoleProvider.FindUsers(
     string groupName,
     IDictionary<string, object> properties
);

IUserCollection IRoleProvider.FindUsers(
     string groupName,
     IDictionary<string, object> properties,
     string extraData
);

The following table defines each argument:

Argument

Description

groupName

When groupName is provided, FindUsers() will return all users within the provided group. Can be null or empty. When groupName isn't provided, the properties argument will contain search criteria.

properties

Contains a dictionary of search parameters of which IUsers to return. Each key in the dictionary corresponds to an item in the Properties dictionary of the IUser. Each key is always provided, but the value is only provided when a search is to be performed on that property. A wildcard character, the asterisks (*), is provided depending on the type of search to be performed. A search can have multiple wildcards.

extraData

While AuthInit and RoleInit provide static provider configuration, extra data can provide dynamic metadata to modify the behavior of the security provider if needed. This value is typically null.

Returns

Returns a collection of IUser objects that are a member of the group groupName OR a collection of all groups that match the search criteria specified in the properties argument.

When the groupName argument is provided, it will contain the exact name of the group to search for. This means that the search criterion is not needed, so the properties dictionary can be ignored. Likewise, if the groupName argument is null or empty, then a search is being performed, and the exact group name is unknown, so the search criteria will be in the properties dictionary. If both the groupName and the properties dictionary items don't have any values, then a complete wildcard search should be performed of all users in the underlying security provider data store.

Searching for Groups

The IRoleProvider.FindGroups() functions very similar to the IRoleProvider.FindUsers() previously described. The following code uses the two interface signatures; one requires two arguments, and the other requires three:

  • userName

  • properties

  • extraData

IGroupCollection FindGroups(
     string userName,
     IDictionary<string, object> properties
)
IGroupCollection FindGroups(
     string userName,
     IDictionary<string, object> properties,
     string extraData
);

The following table describes each argument:

Argument

Description

userName

When userName is provided, FindGroups() will return all the groups the user is a member of. Can be null or empty. When userName isn't provided, the properties argument will contain search criteria.

properties

Contains a dictionary of search parameters of which IGroups to return. Each key in the dictionary corresponds to an item in the Properties dictionary of the IGroup. Each key is always provided, but the value is only provided when a search is to be performed on that property. A wildcard character, the asterisks (*), is provided depending on the type of search to be performed. A search can have multiple wildcards.

extraData

While AuthInit and RoleInit provide static provider configuration, the extraData can provide dynamic metadata to modify the behavior of the security provider if needed. This value is typically null.

Returns

Returns a collection of IGroup objects that the specified user is a member of OR a collection of all groups that match the search criteria specified in the properties argument.

When the userName argument is provided to the FindGroups() method, it will contain an exact username that was previously resolved. This allows for a direct retrieval of the user, and the properties dictionary of search parameters can be ignored. When the userName argument is null or empty, then the search parameters will contain the attributes to search on that match the properties of the IGroup.Properties dictionary. If the userName argument is empty and there are no values for any of the keys in the properties dictionary, then FindGroups should return all groups from the underlying security provider data store.

How K2 Workspace Searches a Security Provider

To demonstrate how searching works between K2 and any security provider, we look to the K2 Workspace as it contains Search functionality in many places.

For an example of where IRoleProvider.FindUsers() and IRoleProvider.FindGroups() are called, see Figure 17-5, which shows the K2 Workspace Search Screen that is used to assign Process Rights to users and groups.

Figure 17-5

Figure 17.5. Figure 17-5

When the user fills out the search criteria and clicks the Search button, the K2 Workspace then calls the K2 server and asks the security provider for the registered label, in this case the provider registered with the security label K2, to do a partial search for all users and groups whose name starts with "Adm". Both FindUsers() and FindGroups() are called. The groupName and userName arguments will both be null; the properties dictionary for both will contain all the properties, but only the Name property will have a value set; and based on the screen above the value for Name will be "Adm*" because the Search Criteria drop-down has the "Starts With" option selected. Notice it found both a user named "Administrator" and a group called "Administrators."

Four possible options control where the wildcard will show up (as shown in Figure 17-6).

Figure 17-6

Figure 17.6. Figure 17-6

The Workspace will add a wildcard in different places, depending on which item is selected in the drop-down shown in Figure 17-6:

  • Starts With: The wildcard will be at the end of the phrase.

  • Ends With: The wildcard will be at the beginning of the phrase.

  • Contains: There will be two wildcards — one at the beginning and one at the end.

  • Equal To: No wildcard will be provided.

Make sure to convert the asterisks' wildcard to the appropriate search symbol used by the underlying security provider data store. For example, if you are passing the phrase with the asterisks' wildcard to an LDAP-based system, the asterisks will work just fine as the wildcard. However, if SQL Server is the underlying data store, the asterisks will need to be converted to percent (%) characters before they can be passed along.

Resolving Destination Queues

Destination queue functionality is a remnant from K2.net 2003 so the functionality provided in IRoleProvider.ResolveQueue() is used to maintain backward compatibility with K2.net 2003. In the case where you have upgraded a K2.net 2003 process or have migrated a K2.net 2003 process to K2 blackpearl, when a process utilizes a destination queue it will call IRoleProvider.ResolveQueue(). In most situations for many custom security providers used in K2.net 2003, it's rare that this method will need to be implemented.

Once K2 blackpearl SP2 is released, any custom security provider built for K2.net 2003 should be able to be migrated to K2 blackpearl under its own security label. Some manual steps will need to be performed in K2 blackpearl to register the custom security provider from K2.net 2003 into K2 blackpearl.

The following table briefly describes how IRoleProvider.ResolveQueue() should function:

Argument

Description

data

This string contains an XML document that represents the destination queue.

Returns

An array of users that are resolved in the queue based on the XML document data.

The following code shows a sample of the DestinationQueues XML document:

<DestinationQueues>
  <DestinationQueue>
    <SendToBehaviour Name="Send To" Type="sendto">
      <SendToItem Name="DENALLIXSales " Type="group"
                Path="LDAP://CN=Sales,CN=Users,DC=denallix,DC=com" />
      <SendToItem Name="DENALLIXOperations" Type="organizationalunit"
                Path="LDAP://OU=Operations,OU=Depts,DC=denallix,DC=com" />
</SendToBehaviour>
    <SendToBehaviour Name="Send To Manager" Type="sendtomanager">
      <SendToItem Name="DENALLIXjanet" Type="user"
        Path="LDAP://CN=Janet Brown,OU=Operations,OU=Depts,DC=denallix,DC=com"         />
    </SendToBehaviour>
  </DestinationQueues>
</DestinationQueue>

The DestinationQueues XML document contains the following elements:

  • A Root element called DestinationQueues — This contains one or more DestinationQueue elements.

  • The DestinationQueue node can contain one or more SendToBehaviour elements.

  • SendToBehaviour elements can contain one or more SendToItem elements and will have a Name attribute and Type attribute. The attribute Type can be sendto, sendtomanager, sendtopeers, or exclude. This behavior element defines rules for how the SendToItem elements are to be resolved.

  • The SentToItem element does not contain any child elements or nodes. It has a Name, Type, and Path attribute. The Name attribute will contain the name of the user, group, or organizational unit that the SendToItem will operate. The Type specifies if the SendToItem is a user, group, or organizationalunit. The Path attribute will contain the path to the SendToItem.

An implementation of IRoleProvider.ResolveQueue() will need to consume the DestinationQueues XML document and resolve all the users based on the behaviors defined in the SendToBehaviour elements using the SendToItems elements.

Remotely Invoking the Security Provider

All of the code covered thus far runs server-side within the context of the Host Server. However, sometimes it's necessary to be able to communicate with a security provider from outside of K2 blackpearl. K2 blackpearl provides an API to remotely query to the security providers from any client, locally or remotely over the network.

Adding a reference to SourceCode.Security.UserRoleManager.Client.dll assembly will provide access to the UserRoleManagerServer object that provides the ability to interface to all security providers on the K2 blackpearl server.

All the methods contained within the UserRoleManagerServer object almost exactly mirror the methods on the IRoleProvider interface. The difference is that the UserRoleManagerServer is agnostic to the security provider being used meaning you can use it to query any of the installed security providers by providing the security label.

The following table documents the methods on the UserRoleManagerServer:

Methods

Description

FindGroups

Overloaded. Invokes IRoleProvider.FindGroups() on the security provider for the specified security label.

FindUsers

Overloaded. Invokes IRoleProvider.FindUsers() on the security provider for the specified security label.

FormatItemName

Invokes IRoleProvider.FormatItemName() on the security provider for the specified security label.

GetDefaultLabelName

Retrieves the default security label name by querying the [SecurityLabels] table in the [HostServer] database where [DefaultLabel] is set to 1.

GetGroup

Overloaded. Invokes IRoleProvider.GetGroup() on the security provider for the specified security label.

GetLabelProvider

Returns a string that contains the name of the provider.

GetLabels

Returns a string array containing all the registered security labels in the Host Server

GetUser

Overloaded. Invokes IRoleProvider.GetUser() on the security provider for the specified security label.

QueryGroupProperties

Invokes IRoleProvider.QueryGroupProperties() on the security provider for the specified security label.

QueryUserProperties

Invokes IRoleProvider.QueryUserProperties() on the security provider for the specified security label.

ResolveQueue

Overloaded. These methods are used mainly to support migrated or upgraded K2.net 2003 processes that use destination queues.

The following is an example of how to use the UserRoleManagerServer object to query the default security provider to discover the groups the user "bob" is in:

// Create a dictionary of search terms with the criteria for
// searching for a group or groups. This isn't used in this
// scenario, but it is required on the interface.
IDictionary<string,object> groupProps = new IDictionary<string,object>();
groupProps.Add("Name", null); // no search criteria needed
groupProps.Add("Description", null); // no search criteria needed

// Build a connection string to the K2 blackpearl server
SCConnectionStringBuilder connBuilder = new SCConnectionStringBuilder();
connBuilder.Host = "DLX";
connBuilder.Port = 5555;
connBuilder.Integrated = true;
// Instantiate the UserRoleManagerServer
UserRoleManagerServer roleServer = new UserRoleManagerServer();
roleServer.CreateConnection();

try
{
    roleServer.Connection.Open(connBuilder.ToString());
    // Make sure bob's username is normalized, this will prefix the
    // default domain in front of the username if it's not there already
    // E.g. Denallixob
    string userName = roleServer.FormatItemName("bob", "K2");
    // Resolve the Active Directory Groups Bob is in
    IGroupCollection bobsGroups =
                           roleServer.FindGroups(userName, groupProps, "K2");
}
finally
{
    roleServer.Connection.Close();
    roleServer.Connsection.Dispose();
}

Single Sign-On (SSO)

With the basic security provider API covered, an additional important feature of K2 blackpearl's Security Framework worth mentioning is Single Sign-On (SSO). SSO solves an important issue created when accessing many systems that have different authentication mechanisms or even different credential requirements through K2 blackpearl. The role of SSO is to broker all authentication requests to third-party systems on behalf of the logged-in K2 user.

Each user has a SSO Cache that stores their personal set of credentials for access to external systems through K2 blackpearl. The SSO Cache stores the security label of the security provider, as well as user-associated name and passwords used to connect to these other systems. The credentials are encrypted and then persisted permanently. If a user's password changes, K2 will attempt to log in using their old credentials in the SSO Cache. If the login fails, the underlying K2 Security Framework will throw an exception containing the information needed to prompt the user for the new credentials.

For example, while accessing a SmartObject, users may be required to provide credentials for the system from which the SmartObject retrieves data. The first time the user attempts to access that back-end system through the SmartObject, SSO checks to see if the SSO Cache has the required credentials for that user to log in to that specific system. If SSO cannot find the credentials in the SSO Cache, the application using the SmartObject receives a SourceCode.Hosting.Exceptions.AuthenticationException. This exception will provide two properties that will supply information needed to ask for the proper credentials. These properties are the ProviderFriendlyName, which contains the security label and the ProviderTypeString. Based on the friendly name and the provider type, any consumer can prompt the user for credentials and then create a new connection string. When authenticating to a system where the credentials are handled by SSO, the connection string requires the IsPrimaryLogin property set to false. Finally a call to Authenticate(string connectionString) on the Connection object will need to be made on behalf of the user to validate the new credentials. To validate the secondary login properly, the user must already be authenticated using their primary login (with IsPrimaryLogin set to true).

Once the user logs in, their credentials are then stored in the SSO Cache, which is a table in one of the K2 blackpearl databases. Once cached, all subsequent authentication requests to that system will be made automatically, on behalf of the user. Credentials cannot be deleted out of the SSO Cache.

Since credentials are the keys to the kingdom, so to speak, it's critical they are secured. K2 blackpearl employs strong cryptography to protect these credentials when they are persisted to the cache. An additional feature planned for future K2 blackpearl release will allow users and/or administrators to choose which credentials to allow to be cached. This will provide flexibility as some policies may prohibit the caching of credentials to particular sensitive systems.

At the time this chapter is being written, the SSO functionality currently only works with SmartObjects. In the future, SSO functionality will work with all K2 blackpearl components.

Building a Custom Security Provider

A complete custom security provider has been developed as a companion to this chapter and can be downloaded from www.wrox.com. The security provider is built around Active Directory Application Mode (ADAM), which is basically a standalone lightweight but robust LDAP Service based on Active Directory. ADAM is included with Server 2003 R2 and can be downloaded from Microsoft and installed on Windows Server 2003 and Windows XP.

Instead of covering the specifics of the ADAM implementation here, this section will cover the basics and some general approaches that can be used to build a custom security provider.

Where to Start

To begin developing a custom security provider, Visual Studio 2005 with the K2 Designer for Visual Studio is required. The development environment can be set up either on a standalone Windows XP system targeting a remote K2 blackpearl server, or on a Server 2003 system with K2 server installed locally with all the development tools.

In order to create a custom K2 blackpearl security provider, the following assemblies' references are required: SourceCode.HostClientAPI.dll, which can be added via the Visual Studio Add Reference dialog, and SourceCode.HostServerInterfaces.dll, which is located in the [K2 Install Path]ConfigurationBin folder on the developer workstation where the K2 Designer for Visual Studio is installed. Additionally, when using the logging functionality in the SourceCode.Logging.Logger namespace, add a reference to the SourceCode.Logging assembly located in the Add Reference dialog in Visual Studio.

Code Organization

Since the IHostableSecurityProvider is a conglomeration of many different interfaces, I've found it helpful to organize each interface for the core security provider in four different partial classes; this allows clean separation of each of the different logical code sections, making it easily find different segments of code and prevent the class file from growing unwieldy. Remember, partial classes are merged into one class when it's compiled, so don't duplicate any methods or properties. Also, all fields, properties, methods, events, and so on will be visible to all classes that are members of the same partial class.

Add four class files to the newly created custom security provider project. Name them as follows:

  • CustomSecurityProvider.IAuthenticationProvider.cs

  • CustomSecurityProvider.IHostableSecurityProvider.cs

  • CustomSecurityProvider.IHostableType.cs

  • CustomSecurityProvider.IRoleProvider.cs

Then open each file and change the class declaration from:

class CustomSecurityProvider

To the following:

partial class CustomSecurityProvider

Then for each file, implement the skeleton interface for the corresponding interface in the file name; for example, to add the IAuthenticationProvider interface to the CustomSecurityProvider.IAuthenticationProvider.cs file, open it in Visual Studio and change it from:

partial class CustomSecurityProvider

To the following:

public partial class CustomSecurityProvider : IAuthenticationProvider

Then click the Smart Tag on the IAuthenticaionProvider interface, and choose Explicitly implement interface 'IAuthenticationProvider' (as shown in Figure 17-7).

Figure 17-7

Figure 17.7. Figure 17-7

This will automatically stub in all of the required IAuthenticationProvider interface requirements. Repeat these steps for each interface.

For simplicity, you can choose the "Explicitly implement interface . . . " option for the interfaces that have overlapping identical method signatures. Really, only two specific methods need to be explicitly implemented: IAuthenticationProvider.Init() and IRoleProvider.Init().

Finally, once all the partial classes are created and the stub methods are generated, only the CustomSecurityProvider.IHostableSecurityProvider.cs class needs the interface IHostableSecurityProvider defined, all the other interfaces on the other partial classes can be removed.

Installing the Custom Security Provider

Once all the stub methods are in place, the custom provider can be installed, and debugging can commence, though without all the methods implemented expect to see errors from the K2 Host Server. Don't install an untested security provider on a production server, as any issues with the provider could affect the availability of the K2 Host Server.

Here is the outline of the process required to install and configure a new security provider:

  1. Stage security provider dependencies.

  2. Stage Host Server configuration settings.

  3. Install the Security Provider library.

  4. Configure the security provider.

  5. Debug the security provider.

  6. Test the security provider. Now let's take a deeper look at each of these six steps.

Staging Provider Dependencies and Configuration

If the custom security provider has any dependencies, they must be deployed and configured before integrating the security provider library into the Host Server. For example, if the custom security provider requires that a database be created and populated with data, make sure to stage that first.

After all the external dependencies are set up and configured, stop the K2 blackpearl Service.

Staging Host Server Configuration Settings

After the service is stopped, any K2 blackpearl-specific configuration settings used by the custom security provider need to be installed–for instance, if the security provider depends on Application Configuration settings in the K2HostServer.config, appSettings section or it may have a database connection string stored in the connectionStrings section.

Installing the Security Provider Library

In the K2 blackpearl Host Server folder, there is a subfolder called securityproviders. This is where the assembly that implements the IHostableSecurityProvider interface goes. After stopping the K2 blackpearl server, copy the new security provider assembly into the [K2 Install Folder]K2 blackpearlHost ServerBinsecurityproviders folder. Make sure to include copying any non-K2 assemblies that aren't installed in the GAC that the custom security provider references.

As of K2 blackpearl 0807, all custom assemblies must be registered with the Host Server database. See KB000351 titled "K2 blackpearl 0807 Feature and Functionality Changes" for more information.

Here are the steps to register an assembly in the Host Server database for version 0807 and later:

  1. Compile the security provider with a strong name.

  2. Extract the public key token of the security provider using the Strong Name Utility that is bundled with the .NET Framework SDK like so:

    sn.exe -Tp MySecurityProvider.dll

    The output will look similar to this:

    Microsoft (R) .NET Framework Strong Name Utility  Version 3.5.30729.1
    Copyright (c) Microsoft Corporation.  All rights reserved.
    
    Public key is
    0024000004800000940000000602000000240000525341310004000001000100fb82d933692ba1be803
    6122893716e6aaf5b37646111747a2ac2d65956c7b3b93889a715fd3903f680cfd88e244460429caff1
    5ae539229b920948d6e1a62103b13c20d8915ec5b014a81de4fc67e9c85c3ac916840382a4dce3a4126
    0d828742153b5d967cf9695b11bbc0b16cbdf397bf22302cedb49d3738432f1d8484fca
    
    Public key token is 94606af0c2dd17b7
  3. Add the security provider to the HostServer database using the following SQL Command, replacing MySecurityProvider.dll and 94606af0c2dd17b7 with the actual assembly name and public key token of your signed security provider dll.

    INSERT INTO [HostServer].[dbo].[AssemblyRegistration]
    (
         [AssemblyID],
         [AssemblyName],
         [PublicKeyToken],
         [Enabled])
    VALUES
    (
         newid(),
         'MySecurityProvider.dll',
         '94606af0c2dd17b7'
         ,1
    )
  4. Make sure to set the useassemblyregistration appSetting in the [K2 Install]Host ServerinK2HostServer.config file as follows:

    <appSettings>
    . . .
         <add key="useassemblyregistration" value="true" />
    . . .
    </appSettings>
  5. Restart the K2 Host Server service.

Configuring the Security Provider

Once the Security Provider library has been copied and the configuration settings have been set up, start the K2 blackpearl Service. On service startup, the K2 server polls the securityproviders folder. If the server finds an assembly that contains an object that implements the IHostableSecurityProvider interface, the Host Server will insert a new record in the SecurityProviders table in the HostServer database for the new provider.

To finish integrating the custom security provider into K2, the HostServer database needs to be updated to set the security label name as well as the authentication and roles initialization data (AuthInit and RoleInit columns). Make sure to give the server a little bit of time to record the new security provider in the database before running the following script; the script will fail if the K2 server either doesn't detect the new security provider for some reason, or if it hasn't had enough time to detect and add the record to the SecurityProviders table.

The following sample SQL script can be modified to add a new security label to the HostServer database:

declare @securityProviderName nvarchar(255)
declare @securityLabel nvarchar(255)
declare @authProviderID uniqueidentifier
declare @securityProviderID uniqueidentifier
declare @securityLabelID uniqueidentifier
declare @authInit xml
declare @roleInit xml
declare @defaultLabel bit

USE [HostServer]

-- This is the security label that will be seen in the K2 Workspace
SET @securityLabel = 'Custom Label Name'

-- This is the Class name of the Provider
SET @securityProviderName = 'Custom.Auth.Provider'

-- Generate a new GUID
SET @securityLabelID = newid()

-- Set up the AuthInit xml document used to initialize the
-- Authentication Provider
SET @authInit = '<authInit />'

-- Set up the RoleInit xml document used to initialize the
-- Roles Provider
SET @roleInit = '<roleProvider><init /></roleProvider>'

-- This is not the system default Label
SET @defaultLabel = 0

-- Extract the Authentication SecurityProviderID for the newly
-- added Provider
SELECT
  @authProviderID = [SecurityProviderID]
FROM
  [SecurityProviders]
WHERE
   [ProviderClassName] = @securityProviderName

-- Extract the Roles SecurityProviderID for the newly
-- added Provider
SELECT
  @securityProviderID = [SecurityProviderID]
FROM
  [SecurityProviders]
WHERE
   [ProviderClassName] = @securityProviderName
-- Link the SecurityProvider to a new Security Label
INSERT INTO [SecurityLabels]
(
     [SecurityLabelID],
     [SecurityLabelName],
     [AuthSecurityProviderID],
     [AuthInit],
     [RoleSecurityProviderID],
     [RoleInit],
     [DefaultLabel]
)
VALUES
(
     @securityLabelID,
     @securityLabel,
     @authProviderID,
     @authInit,
     @roleProviderID,
     @roleInit,
     @defaultLabel
)

At this point, the custom security provider is installed. Check the HostServer log file. There should be an entry making note of the security providers that are loaded. If the K2 blackpearl Service starts and then stops, most likely an exception has occurred in one or multiple Init() methods in the provider. These types of issues can be more difficult to troubleshoot because the service isn't up long enough to attach the debugger and the logger may not be up and running to capture messages written to the Logger.

Debugging the Security Provider

To debug a provider, make sure to copy the .pdb file into the [K2 Install Folder]Host Serverinsecurityprovider folder alongside the security provider assembly.

To debug a custom security provider:

  1. Load the security provider project in Visual Studio.

  2. Click the Debug menu and choose Attach to Process.

  3. Find the K2HostServer.exe in the list, and then click Attach.

See Chapter 6 for more information on debugging on the K2 blackpearl server.

If you are developing on a K2 blackpearl server, a decent debugging approach is to configure the Security Provider project in Visual Studio to start the K2 server in Console mode by launching K2HostServer.exe. This will require configuring the K2 Service to run successfully under your developer account instead of the normal K2 blackpearl Service Account. Make sure to stop the K2 Service before using this approach.

To configure Visual Studio to directly start and debug K2HostServer.exe:

  1. Right-click the Custom Security project in the Visual Studio Solution Explorer and choose Properties.

  2. Once the Project Properties load, click the Debug tab on the right-hand side.

  3. Under Start Action, choose Start External Program, and browse to [K2 Install Folder]Host ServerinK2HostServer.exe.

  4. Click the Save toolbar button to save the project settings.

Now when you press the Start Debugging toolbar button in Visual Studio, it will start the K2 Host Server with the debugger attached.

Additionally, if you're developing on the K2 server, you can create a post-build event in Visual Studio to copy the latest built version of the custom security provider to the Host Serverinsecurityproviders folder in the K2 Installation folder. The security provider has been successfully installed.

To configure Visual Studio to copy the latest version of the security provider assembly and debugging symbols (..pdb file) to the securityproviders folder after a successful build:

  1. Right-click the Custom Security Project in the Visual Studio Solution Explorer, and choose Properties.

  2. Once the Project Properties load, click the Build Events tab on the right-hand side.

  3. Click the Edit Post-build button.

  4. Add the following commands replacing the path that follows with the K2 blackpearl installation path:

    copy "$(TargetPath)" "c:program filesk2 blackpearlHost Serverinsecurityproviders"
    copy "$(TargetDir)$(TargetName).pdb" "c:program filesk2 blackpearlHost
    Serverinsecurityproviders"
  5. Make sure that the Run the post-build event is set to On successful build.

  6. Click the Save toolbar button to save the project settings.

  7. On the Build menu in Visual Studio, click Build Solution.

If the files are unable to be copied successfully, a build error will be reported. Check the Build Output window to trouble-shoot post-build event Errors. Most likely, any post-build event errors encountered will be because the K2 Host Service is running and has a handle on the custom security provider file, locking it.

There are a couple of other things to avoid when directly debugging on the server. Do not use the K2 Object Browser in the same Visual Studio session you are debugging. It gets deadlocked and the Host Server will have to be killed. Start a second Visual Studio session and use the Object Browser in there.

Testing the Security Provider

To verify that the provider is working:

  1. Browse to the K2 Workspace and open the K2 Management Server.

  2. Expand the Server node and then the Workflow Server node.

  3. Choose Process Rights.

  4. Click the Security Label drop-down, and verify the new custom Security Provider label is there.

  5. Select the new provider and then click Search. If there are users and/or roles available for the new provider, it will list the users.

  6. Verify the new custom security provider's security label shows up in the User Browser in the K2 Object Browser in Visual Studio, Visio, or MOSS/WSS.

  7. Once you've verified that the new security label is visible in the K2 Workspace and the K2 Object Browser, test the provider by granting it various permissions to different areas of the K2 Workspace.

  8. Finally, create a workflow that uses users and groups from the new security provider. Since there are many different ways to invoke the security providers within K2, make sure to use a broad range of techniques to test a provider. Make sure to include destination queues and dynamic Roles; make sure to also test the resolution of a user's manager.

K2 connect

K2 connect is an additional layer of functionality that sits on top of K2 blackpearl. Initially Version 1 will provide a security provider that will allow connectivity to SAP. The version after that will add additional connectivity to BizTalk Server and any BizTalk Adapter — which includes Siebel eBusiness Applications, Oracle databases, and any adapter built on the WCF LOB Adapter SDK.

See Chapter 23 for more information on K2 connect.

Summary

This chapter covered the Active Directory User Manager's (ADUM) configurable settings for controlling Active Directory behavior and provided in-depth coverage of how to use the security provider API to build a custom security provider and how to externally invoke the security providers via the Client API. Hopefully, this chapter has illustrated the generous amount of flexibility K2 blackpearl offers for building custom security providers. This extensibility allows K2 blackpearl to integrate into almost any home-grown or third-party systems, leveraging workflow deep within the enterprise.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset