Custom Adapters

Introduction

Adapter provides an interface to your datasource. When Penrose receives an operation from the client, depending on your mapping configuration, it will forward the operation to your datasource.

Writing Custom Adapters

All adapters must inherit from the org.safehaus.penrose.connector.Adapter class. You can overwrite the following methods:

  • init() - This method will be called during initialization.
  • bind(primaryKey, password) - Validate the password for the entry with the given primary key.
  • search(filter) - Return the primary keys of all entries matching the given filter.
  • load(filter) - Return the entire values of all entries matching the given filter.
  • add(primaryKey, values) - Add a new entry with the given values.
  • modify(primaryKey, modifications) - Modify the entry with the given primary key with a list of modifications.
  • delete(primaryKey) - Delete the entry with the given primary key.

Depending on your application, you might only need to implement some of the above methods as some type of datasources might not support all of these operations.

Authentication & Authorization

Many applications support using LDAP for authentication and authorization. Apache HTTP server is an example of such applications. See http://httpd.apache.org/docs/2.0/mod/mod_auth_ldap.html. With an appropriate adapter you can make your application authenticate and authorize against your datasource.

When a user logs in to the application with a user ID and credentials (e.g. username and password), first the application will try to authenticate the user. Depending on your application, there are several methods to perform authentication:

  1. Search only: The application will search for the user's info (including password) based on the given user ID, then compare the credentials internally in the application.
  2. Bind only: The application will map the user ID into a DN then perform the bind operation with the supplied credentials.
  3. Search & bind: The application will search for all possible users matching the given user ID, then perform a bind operation using the supplied password.

Make sure your adapter implements the required operations.

Once a user is authenticated, the application will try to find out what groups the user belongs to or what role the user is authorized to perform. Depending on your application, there are several methods to perform authorization:

  1. Authenticated user: All authenticated users are authorized.
  2. User list: All users included in the list are authorized.
  3. Group list: The application will search for all groups to which the user belongs. If at least one of the groups matches the group list then the user is authenticated.
  4. Attribute comparison: The application will check the user's attribute with the specified value. If they matches, then the user is authorized.

The first two methods are performed internally in the application. The remaining methods require you adapter to support search.

Example

The example files can be found in PENROSE_SERVER_HOME/samples/adapter. See DemoAdapter.java.

In this example, the DemoAdapter simulates a database. It stores a collection of entries in memory.

package org.safehaus.penrose.example.adapter;

import org.safehaus.penrose.connector.Adapter;

public class DemoAdapter extends Adapter {

    Map entries = new HashMap();
}

When you add an entry, it stores the data in the collection.

public int add(SourceConfig sourceConfig, Row pk, AttributeValues sourceValues) throws Exception {

    String sourceName = sourceConfig.getName();
    System.out.println("Adding entry "+pk+" into "+sourceName+":");

    for (Iterator i=sourceValues.getNames().iterator(); i.hasNext(); ) {
        String name = (String)i.next();
        Collection values = sourceValues.get(name);
        System.out.println(" - "+name+": "+values);
    }

    if (entries.containsKey(pk)) return LDAPException.ENTRY_ALREADY_EXISTS;

    entries.put(pk, sourceValues);

    return LDAPException.SUCCESS;
}

When a client binds, the supplied password is compared to the stored password.

public int bind(SourceConfig sourceConfig, Row pk, String password) throws Exception {

    String sourceName = sourceConfig.getName();
    System.out.println("Binding to "+sourceName+" as "+pk+" with password "+password+".");

    AttributeValues sourceValues = (AttributeValues)entries.get(pk);
    if (sourceValues == null) return LDAPException.INVALID_CREDENTIALS;

    Object userPassword = sourceValues.getOne("userPassword");
    if (userPassword == null) return LDAPException.INVALID_CREDENTIALS;

    if (!PasswordUtil.comparePassword(password, userPassword)) return LDAPException.INVALID_CREDENTIALS;

    return LDAPException.SUCCESS;
}

Compile the code and put the jar file into PENROSE_SERVER_HOME/lib/ext:

cd PENROSE_SERVER_HOME/samples/adapter
ant clean dist
cp target/penrose-example-adapters.jar ../../lib/ext

Create a new partition by copying the files in PENROSE_SERVER_HOME/samples/adapter/partition into PENROSE_SERVER_HOME/partitions/adapter. Edit PENROSE_SERVER_HOME/conf/server.xml and add these lines:

<server>

  <adapter name="Demo">
    <adapter-class>org.safehaus.penrose.example.adapter.DemoAdapter</adapter-class>
  </adapter>

  <partition name="adapter" path="partitions/adapter"/>

</server>

In the new partition's connection.xml we defined a connection with the DemoAdapter:

<connection name="DemoDatabase">
  <adapter-name>Demo</adapter-name>
</connection>

In the new partition's sources.xml we also defined a source with this connection:

<source name="users">
  <connection-name>DemoDatabase</connection-name>
  <field name="uid" primaryKey="true"/>
  <field name="cn"/>
  <field name="sn"/>
  <field name="userPassword"/>
</source>

In the new partition's mapping.xml we map this source to an entry:

<entry dn="uid=...,ou=Users,dc=Adapter,dc=Example,dc=com">
  <oc>person</oc>
  <oc>organizationalPerson</oc>
  <oc>inetOrgPerson</oc>
  <at name="uid" rdn="true">
    <variable>u.uid</variable>
  </at>
  <at name="cn">
    <variable>u.cn</variable>
  </at>
  <at name="sn">
    <variable>u.sn</variable>
  </at>
  <at name="userPassword">
    <variable>u.userPassword</variable>
  </at>
  <source name="u">
    <source-name>users</source-name>
    <field name="uid">
      <variable>uid</variable>
    </field>
    <field name="cn">
      <variable>cn</variable>
    </field>
    <field name="sn">
      <variable>sn</variable>
    </field>
    <field name="userPassword">
      <variable>userPassword</variable>
    </field>
  </source>
</entry>

Start Penrose, then run the following commands. Make sure you have LDAP command-line clients installed in your PATH.

ant test-search
ant test-add
ant test-search

You will see that the new entry has been added to the in-memory database.

$ ant test-search
Buildfile: build.xml

test-search:
     [exec] # extended LDIF
     [exec] #
     [exec] # LDAPv3
     [exec] # base <dc=Adapter,dc=Example,dc=com> with scope sub
     [exec] # filter: (objectclass=*)
     [exec] # requesting: ALL
     [exec] #

     [exec] # Adapter.Example.com
     [exec] dn: dc=Adapter,dc=Example,dc=com
     [exec] objectClass: dcObject
     [exec] objectClass: organization
     [exec] dc: Adapter
     [exec] o: Adapter

     [exec] # Users, Adapter.Example.com
     [exec] dn: ou=Users,dc=Adapter,dc=Example,dc=com
     [exec] objectClass: organizationalUnit
     [exec] ou: Users

     [exec] # test, Users, Adapter.Example.com
     [exec] dn: uid=test,ou=Users,dc=Adapter,dc=Example,dc=com
     [exec] objectClass: inetOrgPerson
     [exec] objectClass: organizationalPerson
     [exec] objectClass: person
     [exec] uid: test
     [exec] userPassword:: dGVzdA==
     [exec] sn: User
     [exec] cn: Test User

     [exec] # search result
     [exec] search: 2
     [exec] result: 0 Success

     [exec] # numResponses: 4
     [exec] # numEntries: 3

References