<?xml version="1.0"?>
<?xml-stylesheet href="/home/dave/projects/docbook-css/driver.css" type="text/css"?>
<article>
  <title>Installing a Custom OpenSSO Identity Repository</title>
  <articleinfo>
    <author>
      <firstname>David</firstname>
      <surname>Holroyd</surname>
      <email>david.holroyd@goodtechnology.com</email>
      <affiliation>
        <jobtitle>Head of Server-Side Development</jobtitle>
	<orgname>Goodtechnology Ltd.</orgname>
      </affiliation>
    </author>
  </articleinfo>

  <section>
    <title>Introduction</title>

    <para>These notes show how to implement a simple <ulink
    url="https://opensso.dev.java.net/">OpenSSO</ulink> 'Data Store' on top of
    a relational database.  We discuss the coding required and then show how to
    configure OpenSSO to use our plugin using its web-based administration
    console.</para>
  </section>

  <section>
    <title>The Repository</title>

    <para>The situation we want to address is that of needing to provide Web
    Single Sign-On and account management to users defined by an existing
    relational database.  While it is possible to configure a very basic JDBC
    Authentication module in OpenSSO, this does not provide any identity
    management features other than authentication.  To provide OpenSSO with
    'full' access to the identity repository, we must implement a Data Store
    adapter, and configure OpenSSO to use this.</para>

    <para>To simplify the implementation of this example, the user account
    information will be stored in an
    <ulink url="http://www.hsqldb.org/">HSQLDB</ulink> in-memory database.  A
    real application would of course need to deal with the issue of how to
    obtain JDBC configuration (and would probably require that data persist
    between application restarts).</para>

    <section>
      <title>Implementation Basics</title>

      <para>OpenSSO Data Store plugins must implement the class
      <ulink url="http://docs.sun.com/source/819-4682/com/sun/identity/idm/IdRepo.html"><code>com.sun.identity.idm.IdRepo</code></ulink>.
      This class has a number of abstract methods which must be implemented, and
      some other methods whose default implementation must be overriden in
      order to be able to provide a useful service.</para>

      <para>Our IdRepo will be implemented in terms of a simplistic JDBC Data
      Access Object.</para>
    </section>

    <section>
      <title>Data Store Plugin Lifecycle</title>

      <para>In order for OpenSSO to be able to load our plugin, the IdRepo
      subclass must have a no-args constructor for OpenSSO to call.</para>

      <para>When OpenSSO loads our plugin, it will call its
      <ulink url="http://docs.sun.com/source/819-4682/com/sun/identity/idm/IdRepo.html#initialize(java.util.Map)">initialize()</ulink>
      method.  We use this opportunity in our implementation to initialize the
      Data Access Object.</para>

      <para>There is also a chance to perform any cleanup needed when
      OpenSSO is finished with our plugin, by overriding the
      <ulink url="http://docs.sun.com/source/819-4682/com/sun/identity/idm/IdRepo.html#shutdown()">shutdown()</ulink>
      method.</para>

      <para>The IdRepo class also requires that implementations provide
      <ulink href="http://docs.sun.com/source/819-4682/com/sun/identity/idm/IdRepo.html#addListener(com.iplanet.sso.SSOToken,%20com.sun.identity.idm.IdRepoListener)">addListener()</ulink>
      and
      <ulink href="http://docs.sun.com/source/819-4682/com/sun/identity/idm/IdRepo.html#removeListener()">removeListener()</ulink>
      methods.  While it is not entirely clear what these methods are for,
      OpenSSO will not be able to use our plugin correctly if they raise
      exceptions, so we provide basic no-op implementations.</para>

      <programlisting><![CDATA[	public void initialize(Map configParams) {
	      super.initialize(configParams);
	      dao = new IdDAO();
	      try {
		      dao.init();
	      } catch (SQLException e) {
		      e.printStackTrace();
	      }
      }

      public void shutdown() {
	      super.shutdown();
      }

      public int addListener(SSOToken token, IdRepoListener listener)
	      throws IdRepoException, SSOException
      {
	      // TODO: no idea what this needs to do
	      return 0;
      }

      public void removeListener() {
	      // TODO: no idea what this needs to do
      }]]></programlisting>
    </section>

    <section>
      <title>Declaring Capabilities</title>

      <para>Data Stores can have varying capabilities in terms of the kind
      of <wordasword>subjects</wordasword> they support and the operations that
      they will perform on those subjects.  In order to tell OpenSSO what our
      plugin is capable of we need to override the methods
      <ulink url="http://docs.sun.com/source/819-4682/com/sun/identity/idm/IdRepo.html#getSupportedTypes()">getSupportedTypes()</ulink>,
      <ulink url="http://docs.sun.com/source/819-4682/com/sun/identity/idm/IdRepo.html#getSupportedOperations(com.sun.identity.idm.IdType)">getSupportedOperations(IdType)</ulink>
      and
      <ulink url="http://docs.sun.com/source/819-4682/com/sun/identity/idm/IdRepo.html#supportsAuthentication()">supportsAuthentication()</ulink>.</para>

      <para>If you look at the example code, you will see that we implement
      these methods to declare that we only support the <code>USER</code>
      identity-type, the operations <code>CREATE</code>, <code>READ</code> and
      <code>EDIT</code> on these identities, and return <code>true</code> from
      supportsAuthentication().</para>

      <programlisting><![CDATA[	private static final Set SUPPORTED_TYPES;
	static {
		SUPPORTED_TYPES = new HashSet();
		SUPPORTED_TYPES.add(IdType.USER);
	}

	private static final Set SUPPORTED_OPS;
	static {
		SUPPORTED_OPS = new HashSet();
		// TODO: DELETE?  &  SERVICE?
		SUPPORTED_OPS.add(IdOperation.READ);
		SUPPORTED_OPS.add(IdOperation.CREATE);
		SUPPORTED_OPS.add(IdOperation.EDIT);
	}
	
	public Set getSupportedOperations(IdType type) {
		return SUPPORTED_OPS;
	}

	public Set getSupportedTypes() {
		return SUPPORTED_TYPES;
	}

	public boolean supportsAuthentication() {
		return true;
	}]]></programlisting>
    </section>

    <section>
      <title>Implementing the Declared Operations</title>

      <para>Having declaired that we support <code>CREATE</code>,
      <code>READ</code>, <code>EDIT</code> and authentication for OpenSSO
      <code>USER</code> subjects, we need to provide implementations for
      the relevant methods of IdRepo for OpenSSO to call upon.</para>

      <para>Other methods which we must implement (because they are abstract
      in IdRepo), but which provide unsuppored operations are simply implemented
      to raise IdRepoUnsupportedOpException.</para>

      <para>The operations that we do implement all assert that the IdType
      argument they are passed is <code>IdType.USER</code>, enforcing the
      contract we've declared.</para>

      <note>
        <title>SSOToken Arguments</title>

	<para>Most of these methods take an initial argument of type SSOToken.
	This token does not represent the identity upon which the operation
	in question is being performed (that's what the String 'name' is for).
	This SSOToken actually references the identity which is performing the
	operation (i.e. the admin console user, or remote agent).</para>
      </note>

      <section>
        <title>IdOperation.CREATE</title>

	<programlisting><![CDATA[	public String create(SSOToken token, IdType type, String name,
	                     Map attrMap)
		throws IdRepoException, SSOException
	{
		assertUserType(type);
		User newUser = userFromAttributes(name, attrMap);
		try {
			dao.insertUser(newUser);
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
		// TODO: "string representation of created value" wha?
		return "["+name+"]";
	}]]></programlisting>

	<para>Try to produce a useful return value, although appears that the
	current implementation of OpenSSO discards it for all IdRepo
	implementations except its internal one.</para>
      </section>

      <section>
        <title>IdOperation.READ</title>

	<programlisting><![CDATA[	public boolean isExists(SSOToken token, IdType type, String name)
		throws IdRepoException, SSOException
	{
		assertUserType(type);
		try {
			return dao.loadUser(name) != null;
		} catch (SQLException e) {
			throw new IdRepoFatalException(e.toString());
		}
	}
	
	public boolean isActive(SSOToken token, IdType type, String name)
		throws IdRepoException, SSOException
	{
		// We could load the entry from the database to check a status
		// flag, but our simple schema doesn't define one; all user
		// accounts are always enabled. 
		assertUserType(type);
		return true;
	}

	public Map getAttributes(SSOToken token, IdType type, String name)
			throws IdRepoException, SSOException
	{
		assertUserType(type);
		User user;
		try {
			user = dao.loadUser(name);
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
		return toAttrs(user);
	}

	public Map getAttributes(SSOToken token, IdType type, String name, Set attrNames)
		throws IdRepoException, SSOException
	{
		// TODO: should filter the results to reflect requested attrs,
		// but for the moment, lets just return then all,
		return getAttributes(token, type, name);
	}

	public Map getBinaryAttributes(SSOToken token, IdType type, String name, Set attrNames)
		throws IdRepoException, SSOException
	{
		assertUserType(type);
		// we don't have any binary attributes,
		return Collections.EMPTY_MAP;
	}

	public RepoSearchResults search(SSOToken token, IdType searchType,
			String pattern, int maxTime, int maxResults,
			Set returnAttrs, boolean returnAllAttrs, int filterOp,
			Map avPairs, boolean recursive)
		throws IdRepoException, SSOException
	{
		assertUserType(searchType);
		if ("*".equals(pattern)) {
			// TODO: other 'glob'-like matches allowed?
			pattern = null;
		}
		List users;
		Map queryParams = avPairs==null ? null : remap(avPairs);
		try {
			users = dao.search(pattern, maxResults, queryParams, filterOp);
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
		Set searchResults = toDNSet(users);
		int errorCode = RepoSearchResults.SUCCESS;
		Map resultsMap = toEntryAttributeSetMap(users);
		return new RepoSearchResults(searchResults,
		                             errorCode,
		                             resultsMap,
		                             searchType);
	}
]]></programlisting>
      </section>

      <section>
        <title>IdOperation.EDIT</title>

	<programlisting><![CDATA[	public void setAttributes(SSOToken token, IdType type, String name, Map attributes, boolean isAdd)
		throws IdRepoException, SSOException
	{
		// TODO: not sure what isAdd indicates
		assertUserType(type);
		try {
			// load user and copy changed properties
			User user = dao.loadUser(name);
			Map props = attrMapToPropMap(attributes);
			copyProperties(user, props);
			dao.updateUser(user);
		} catch (SQLException e) {
			e.printStackTrace();
			throw new IdRepoException(e.toString());
		}
	}

	public void setBinaryAttributes(SSOToken token, IdType type, String name, Map attributes, boolean isAdd)
		throws IdRepoException, SSOException
	{
		assertUserType(type);
		// don't support any binary attributes, so nothing to do
	}

	public void removeAttributes(SSOToken token, IdType type, String name, Set attrNames)
		throws IdRepoException, SSOException
	{
		assertUserType(type);
		// at the moment, there are no attributes that can resonably be
		// removed.  We could in theory set the columns to 'null' in
		// the database, except we've deliberately defined the columns
		// to dissallow this.
	}]]></programlisting>
      </section>

      <section>
        <title>Authentication</title>

	<programlisting><![CDATA[	public boolean authenticate(Callback[] credentials) throws IdRepoException, AuthLoginException {
		// Obtain user name and password from credentials and authenticate
		String username = null;
		String password = null;
		for (int i = 0; i < credentials.length; i++) {
			Callback cred = credentials[i];
			if (cred instanceof NameCallback) {
				username = ((NameCallback)cred).getName();
			} else if (cred instanceof PasswordCallback) {
				char[] passwd = ((PasswordCallback)cred).getPassword();
				if (passwd != null) {
					password = new String(passwd);
				}
			}
		}
		if (username == null || password == null) {
			Object args[] = { getClass().getName() };
			throw new IdRepoException(IdRepoBundle.BUNDLE_NAME, "221", args);
		}
		User user;
		try {
			user = dao.loadUser(username);
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
		return user != null && password.equals(user.getPassword());
	}]]></programlisting>
      </section>
    </section>

    <section>
      <title>Implementing the Rest</title>

      <para>To see the utility methods used in implementing the public
      interface above, and the the source code for the Data Access Object,
      take a look at the full source of
      <ulink url="JDBCIdRepo.java">JDBCIdRepo</ulink>,
      <ulink url="IdDAO.java">IdDAO</ulink>,
      <ulink url="User.java">User</ulink> and
      <ulink url="PropertyNameRemapper.java">PropertyNameRemapper</ulink>.</para>
    </section>

    <section>
      <title>Usage patterns</title>

      <para>This section attempts to describe how OpenSSO will actually use
      our plugin to perform different actions.</para>

      <section>
	<title>Authentication</title>

	<para>The authentication process is controlled by OpenSSO authentication
	plugins, not by the the identity repository itself.  The section below
	will show how to configure OpenSSO to check user supplied credentials
	against our repository plugin using Data Store authentication.</para>

	<para>The DataStore auth module requires the user enter a name and
	password.  Once these are collected, it will pass them to our IdRepo to
	be checked by the <code>authenticate()</code> method.  A custom
	authentication plugin could be built to request alternative
	credentials, some of which which could then be passed to the
	IdRepo.</para>

	<para>Whichever authentication module is used, it will, after
	successfull authentication, create an object implementing the
	<ulink url="http://java.sun.com/j2se/1.5.0/docs/api/java/security/Principal.html">Principal</ulink>
	interface to represent the identity.</para>

	<para>It is the value returned by this Principal object's getName()
	method which you will later see being passed as the 'name' argument to
	the various IdRepo methods we implemented earlier.</para>
      </section>

      <section>
	<title>Identity Profile Creation</title>

	<para>If, as in the example configuration, we are going to require that
	users have a profile in the IdRepo, OpenSSO will try to load this after
	successful auth.  The IdRepo API doesn't let OpenSSO ask directly what
	type of identity as associated with the supplied credentials.
	OpenSSO will query the reposotory, potentially multiple times, to try
	and find the named identity under different IdTypes.  This means that
	after a successful authentication, the IdRepo will immidiately see
	calles to its <code>search()</code> implementation.</para>
      </section>
    </section>
  </section>











  <section>
    <title>Adding the Repository Plugin into the OpenSSO Server</title>

    <para>Having created your basic implementation of <code>IdRepo</code>,
    you will need to 'install' it into an OpenSSO server to see if it
    works.</para>

    <para>In the terminology OpenSSO admin console, we need to add our new
    code as a 'data store' for our realm.</para>

    <warning>
      <title>Avoid the opensso Realm</title>

      <para>The OpenSSO server ships with a realm called 'opensso' which
      is used for managing access to OpenSSO itself (e.g. logging in to the
      admin console).  Do not perform experiments with this realm; instead,
      create a new sub-realm to avoid locking yourself out of OpenSSO if things
      go wrong.</para>
    </warning>

    <para>To make the plugin available, it's necessary to pack it in to
    the opensso Web Application Archive file.  We can achieve this by adding
    a fragment of Ant script like the following to our build:</para>

    <programlisting><![CDATA[	<target name="repack-opensso-war" depends="package">
		<mkdir dir="build/opensso-tmp"/>
		<unwar src="${opensso-war}" dest="build/opensso-tmp"/>
		<war destfile="build/opensso.war" webxml="build/opensso-tmp/WEB-INF/web.xml">
			<fileset dir="build/opensso-tmp"/>
			<lib file="${idrepo-jarname}"/>
                        <lib file="${jar.hsqldb}"/>
                        <lib file="${jar.beanutils}"/>
		</war>
	</target>]]></programlisting>

    <section>
      <title>Create a new Realm</title>

      <para>We will create a realm to test the IdRepo implementation within.
      Log in to the admin OpenSSO console and click
      <guibutton>New..</guibutton> in 'Realms':</para>

      <informalfigure>
        <mediaobject>
	  <imageobject>
	    <imagedata fileref="images/realms_new.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>

      <para>On the subsequent 'New Realm' screen, enter a name for the realm,
      and select <guibutton>OK</guibutton>, leaving all other settings at
      their default values:</para>

      <informalfigure>
        <mediaobject>
	  <imageobject>
	    <imagedata fileref="images/new_realm.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>
    </section>

    <section>
      <title>Adding the Data Store to the new Realm</title>

      <para>Select the realm which we just added:</para>

      <informalfigure>
        <mediaobject>
	  <imageobject>
	    <imagedata fileref="images/realms_select_test.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>

      <para>Now, in the configuration screen for the 'test' realm, select the
      'Data Stores' tab:</para>

      <informalfigure>
        <mediaobject>
	  <imageobject>
	    <imagedata fileref="images/realm_test_select_datastores.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>

      <para>Now click <guibutton>New..</guibutton> in the 'Data Stores'
      section:</para>

      <informalfigure>
        <mediaobject>
	  <imageobject>
	    <imagedata fileref="images/datastores_new.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>

      <para>We are now prompted for the name and type for the new datastore.
      The type selected must be
      <guilabel>Access Manager Repository Plug-in</guilabel>.  After entering
      a name, click <guibutton>Next</guibutton>:</para>

      <informalfigure>
        <mediaobject>
	  <imageobject>
	    <imagedata fileref="images/datastore_new_select_type.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>

      <para>The configurables for the repository plugin must now be supplied.
      On the step 2 screen, enter the name of our IdRepo implementation class.
      Leaving all other configuration settings as they are, click
      <guibutton>Finish</guibutton>:</para>

      <note>
	<title>Advanced Data Store Configuration</title>

	<para>To make your Data Store a 'first class' entry in the list of
	Data Store types, and to gain the possiblility of providing for custom
	parameters to be added via the admin console, the file
	<ulink url="https://opensso.dev.java.net/source/browse/*checkout*/opensso/products/amserver/xml/services/idRepoService.xml"><filename>opensso/products/amserver/xml/services/idRepoService.xml</filename></ulink>
	may be updated with a descriptor for your newly implemented data
	store.</para>

	<para>This example is so trivial that it doesn't require any extra
	configuration data.</para>
      </note>

      <informalfigure>
        <mediaobject>
	  <imageobject>
	    <imagedata fileref="images/datastore_new_conf_plugin.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>

      <para>If there are any problems loading this plugin, exception
      stacktraces will appear in
      <filename><replaceable>$CONFIG_DIR</replaceable>/opensso/debug/amIdm</filename>
      (assuming you have debugging emabled in
      <filename><replaceable>$CONFIG_DIR</replaceable>/AMConfig.properties</filename>).</para>
      <para>If, once loaded, the console has problems using the plugin,
      exception stacktraces will be logged to
      <filename><replaceable>$CONFIG_DIR</replaceable>/opensso/debug/amConsole</filename></para>

      <para>Finally, delete the default flat-file datastore that was created
      with the realm by ticking the box next to 'files' in the Data Stores
      list and then clicking <guibutton>Delete</guibutton></para>

      <informalfigure>
        <mediaobject>
	  <imageobject>
	    <imagedata fileref="images/realm_test_delete_flatfile.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>
    </section>

    <section>
      <title>A Little Test of the Data Store</title>

      <para>Select the 'Subjects' tab within the 'test' realm configuration.
      Ensure that things like adding new users and selecting / editing
      users attiributes works as expected:</para>

      <informalfigure>
        <mediaobject>
	  <imageobject>
	    <imagedata fileref="images/realm_test_subjects.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>

      <para>Note that the sub-tabs appearing under the 'Subjects' tab should
      corespond with the values returned by the
      <code>getSupportedTypes()</code> method of your <code>IdRepo</code>
      implementation.</para>
    </section>
  </section>

  <section>
    <title>Authenticating Against the Data Store</title>

    <para>Having added our new datastore, we now need to configure our
    realm to use this datastore for authentication.</para>

    <section>
      <title>Creating the Authentication Chain</title>

      <para>For our 'test' realm, select the 'Authentication' tab:</para>
      <informalfigure>
	<mediaobject>
	  <imageobject>
	    <imagedata fileref="images/realm_test_select_authentication.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>

      <para>On the 'Authentication' page, scrolling down past the 'General'
      section should reveal the 'Module Instances'.  Select
      <guibutton>New</guibutton> for 'Module Instances':</para>

      <informalfigure>
	<mediaobject>
	  <imageobject>
	    <imagedata fileref="images/authentication_moduleinstances_new.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>

      <para>On the New Modual Instance page, enter the name 'testDataStore',
      select type 'Data Store', and select <guibutton>OK</guibutton>:</para>

      <informalfigure>
	<mediaobject>
	  <imageobject>
	    <imagedata fileref="images/new_auth_module_instance.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>

      <para>Back on the 'test' realm authentication page, we now need to
      create an authentication chain making use of the authentication
      module instance we just created.  Select <guibutton>New</guibutton>
      in the 'Authentication Chaining' area:</para>

      <informalfigure>
	<mediaobject>
	  <imageobject>
	    <imagedata fileref="images/authentication_authchaining_new.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>

      <para>On the 'New Authentication Chain' page, enter a name, 'testservice'
      for the new chain, and select <guibutton>OK</guibutton>:</para>

      <informalfigure>
	<mediaobject>
	  <imageobject>
	    <imagedata fileref="images/new_authentication_chain.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>

      <para>The next page allows us to specify properties for the new chain.
      There are are initially no entries in the chain, so select
      <guibutton>Add</guibutton>:</para>

      <informalfigure>
	<mediaobject>
	  <imageobject>
	    <imagedata fileref="images/new_authentication_chain_properties_addentry.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>

      <para>A new entry will be added to the list.  This entry should have
      'Instance' set to 'testDataStore' and 'Criteria' set to 'REQUIRED'.
      Select <guibutton>Save</guibutton> and then
      <guibutton>Back to Authentication</guibutton> to finish the new
      authentication chain:</para>

      <informalfigure>
	<mediaobject>
	  <imageobject>
	    <imagedata fileref="images/new_authentication_chain_properties_addentry_save.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>
      
      <para>Finally, remove the default LDAP authentication configuration
      by deleting first the 'ldapservice' authentication chain, and then
      the 'LDAP' module instance:</para>

      <informalfigure>
	<mediaobject>
	  <imageobject>
	    <imagedata fileref="images/authentication_delete_ldap.png" format="PNG"/>
	  </imageobject>
	</mediaobject>
      </informalfigure>

      <note>
        <title>Zombie LDAP Module</title>
	<para>After deleting things as above, I see the LDAP authentication
	module reappear again in the admin console.  I've yet to work out why
	this is, or if it actually causes any problems.</para>
      </note>
    </section>

    <section>
      <title></title>

      <para></para>

    </section>
  </section>
</article>

<!--
vim:sts=2:sw=2
-->
