You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 20 Next »

Be Careful

Work in progress. Any feedback highly appreciated!

Implementing a simple interceptor

The following is for developers who plan to implement their own interceptors in order to extend or modify the functionality of Apache Directory Server. It contains a simple example as a starting point.

What exactly is an interceptor?

An interceptor filters method calls performed on on the DefaultPartitionNexus just like Servlet filters do. The ApacheDS configuration contains a chain of filters performing several tasks. In order to illustrate this, here is the list of interceptors from the default server configuration of ApacheDS 1.5.5

  • org.apache.directory.server.core.normalization.NormalizationInterceptor
  • org.apache.directory.server.core.authn.AuthenticationInterceptor
  • org.apache.directory.server.core.referral.ReferralInterceptor
  • org.apache.directory.server.core.authz.AciAuthorizationInterceptor
  • org.apache.directory.server.core.authz.DefaultAuthorizationInterceptor
  • org.apache.directory.server.core.exception.ExceptionInterceptor
  • org.apache.directory.server.core.changelog.ChangeLogInterceptor
  • org.apache.directory.server.core.operational.OperationalAttributeInterceptor
  • org.apache.directory.server.core.schema.SchemaInterceptor
  • org.apache.directory.server.core.subtree.SubentryInterceptor
  • org.apache.directory.server.core.collective.CollectiveAttributeInterceptor
  • org.apache.directory.server.core.event.EventInterceptor
  • org.apache.directory.server.core.trigger.TriggerInterceptor
  • org.apache.directory.server.core.journal.JournalInterceptor

Interceptors should usually pass the control of current invocation to the next interceptor by calling an appropriate method on NextInterceptor. The flow control is returned when the next interceptor's filter method returns. You can therefore implement pre-, post-, around- invocation handler by how you place the statement.

Interceptors are a powerful way to extend and modify the server behavior. But be warned. A mistakenly written interceptor may lead to a dis-functional or corrupt server.

Password hash. A simple interceptor

In order to demonstrate how to write an interceptor, here is a simple but realistic example. The following requirement should be fulfilled by an interceptor.

  • No user password should be stored in the directory in clear text.

To be more concrete:

  • If a userpassword is set by an LDAP client in plain text, a message digest algorithm should be applied to the value, and the one-way encrypted value should be stored
  • the algorithm should be applied if new entries are created or existing entries are modified (hence modify and add operations will be intercepted)
  • If the value given by the client is already provided in hashed form, nothing happens, and the given value is tored in the directory without modification

The sources

Currently, the sources are checked in here

In order to build it, simply check it out and type "mvn install".

Implementing the class PasswordHashInterceptor

The following UML class diagram depicts the structure of the little example.

The class HashTools contains two simple methods w.r.t. hashing. isAlreadyHashed detects whether a value has already been hashed with a known message digest algorithm. applyHashAlgorithm applies a hash algorithm to a sequence of bytes. See the source code and the unit tests of this class for details, it has not that much to do with the interceptor stuff.

The central class is PasswordHashInterceptor. Every interceptor has to implement the Interceptor interface from package org.apache.directory.server.core.interceptor. PasswordHashInterceptor does so by extended the convenience class BaseInterceptor from the same package.

package org.apache.directory.samples.interceptor.pwdhash;

import static org.apache.directory.samples.interceptor.pwdhash.HashTools.applyHashAlgorithm;
import static org.apache.directory.samples.interceptor.pwdhash.HashTools.isAlreadyHashed;

import java.util.List;
import java.util.Set;

import org.apache.directory.server.core.entry.ClonedServerEntry;
import org.apache.directory.server.core.interceptor.BaseInterceptor;
import org.apache.directory.server.core.interceptor.NextInterceptor;
import org.apache.directory.server.core.interceptor.context.AddOperationContext;
import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
import org.apache.directory.shared.ldap.entry.EntryAttribute;
import org.apache.directory.shared.ldap.entry.Modification;
import org.apache.directory.shared.ldap.entry.ModificationOperation;
import org.apache.directory.shared.ldap.schema.AttributeType;

public class PasswordHashInterceptor extends BaseInterceptor {

    private String passwordAttributeName = "userPassword";

    private String hashAlgorithm = "MD5";

    public void setPasswordAttributeName(String passwordAttributeName) {
        this.passwordAttributeName = passwordAttributeName;
    }

    public void setHashAlgorithm(String hashAlgorithm) {
        this.hashAlgorithm = hashAlgorithm;
    }

    /**
     * Intercepts the modify operation in order to replace plain password values
     * with hashed ones.
     */
    @Override
    public void modify(NextInterceptor next, ModifyOperationContext opContext)
            throws Exception {

        List<Modification> items = opContext.getModItems();
        for (Modification modification : items) {
            if (modification.getOperation() == ModificationOperation.ADD_ATTRIBUTE
                    || modification.getOperation() == ModificationOperation.REPLACE_ATTRIBUTE) {
                EntryAttribute attribute = modification.getAttribute();
                if (attribute.getId().equalsIgnoreCase(passwordAttributeName)) {
                    hashPasswordIfNeccessary(attribute);
                }
            }
        }
        super.modify(next, opContext);
    }

    /**
     * Intercepts the add operation in order to replace plain password values
     * with hashed ones.
     */
    @Override
    public void add(NextInterceptor next, AddOperationContext opContext)
            throws Exception {

        ClonedServerEntry entry = opContext.getEntry();
        Set<AttributeType> attributeTypes = entry.getAttributeTypes();
        for (AttributeType attributeType : attributeTypes) {
            if (attributeType.getName().equalsIgnoreCase(passwordAttributeName)) {
                EntryAttribute attribute = entry.get(attributeType);
                hashPasswordIfNeccessary(attribute);
            }
        }

        super.add(next, opContext);
    }

    protected void hashPasswordIfNeccessary(EntryAttribute attribute) {
        try {
            byte[] password = attribute.getBytes();
            if (!isAlreadyHashed(password)) {
                byte[] hashed = applyHashAlgorithm(hashAlgorithm, password);
                attribute.clear();
                attribute.add(hashed);
            }
        } catch (Exception e) {
            throw new RuntimeException("Password hash failed", e);
        }
    }
}

Using the interceptor

Embedded mode

package org.apache.directory.samples.interceptor.pwdhash;

import java.util.List;

import org.apache.directory.server.core.DefaultDirectoryService;
import org.apache.directory.server.core.DirectoryService;
import org.apache.directory.server.core.interceptor.Interceptor;
import org.apache.directory.server.ldap.LdapServer;
import org.apache.directory.server.protocol.shared.transport.TcpTransport;

/**
 * Main class which starts an embedded server with the interceptor added to the
 * chain.
 */
public class Main {

    public static void main(String[] args) throws Exception {

        DirectoryService directoryService = new DefaultDirectoryService();
        directoryService.setShutdownHookEnabled(true);

        LdapServer ldapServer = new LdapServer();
        ldapServer.setDirectoryService(directoryService);
        ldapServer.setAllowAnonymousAccess(true);

        List<Interceptor> is = directoryService.getInterceptors();
        is.add(new PasswordHashInterceptor());
        directoryService.setInterceptors(is);

        TcpTransport ldapTransport = new TcpTransport(10389);
        ldapServer.setTransports(ldapTransport);

        directoryService.startup();
        ldapServer.start();
    }
}

Adding it to a server.xml file

TBD.

Verification

Let's check whether our new interceptor does its job! In order to do so, we use Apache Directory Studio and connect to the server with the interceptor enabled (see above).

First we create a new entry with the following data

dn: cn=Kate Bush,ou=users,ou=system
objectClass: person
objectClass: top
cn: Kate Bush
sn: Bush

Then we add a new attribute userPassword in the entry editor. For the value, a special editor appears:

select Plaintext as the hash method and enter a new password. We selected "secret" (see above). After pressing OK, a modify operation is sent to the server, which will be intercepted by our example class.

Afterwards, the value for userPassword is not "secret", but the MD5 digested value of it.

Kate Bush is still capable of authenticating with the password "secret", because Apache Directory Server supports storing passwords hashed with this algorithm.

Limitations of the example

TBD.

Further reading

  • No labels