Tutorial04 - Building a Custom Authenticator

Introduction

Datameer has built in user and group management that is used for authentication. However if you want to plug in your own authenticator into Datameer you can write a plug-in and create a class that implements datameer.dap.sdk.authentication.AuthenticatorExtension. All such additional authenticator extensions are shown in the Admin tab > Authentication and an administrative user can select the authenticator that should be used.

Example Authenticator

You build a very simple authenticator that grants access to an analyst user john/doe. In addition user accounts for administration, users should be configurable on the authenticator set-up screen.

Here is the implementation for the TutorialAuthenticatorExtension:

TutorialAuthenticatorExtension.java
package datameer.das.plugin.tutorial04;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import datameer.dap.sdk.authentication.AbstractAuthenticator;
import datameer.dap.sdk.authentication.AuthenticatorExtension;
import datameer.dap.sdk.common.GenericConfiguration;
import datameer.dap.sdk.property.PropertyDefinition;
import datameer.dap.sdk.property.PropertyGroupDefinition;
import datameer.dap.sdk.property.PropertyType;
import datameer.dap.sdk.property.WizardPageDefinition;

public class TutorialAuthenticatorExtension extends AuthenticatorExtension {

    static final String PASSWORD = "password";
    static final String NAME = "name";

    /**
     * Returns the extension id. This must be unique for all extensions.
     * 
     * @return the extension id.
     */
    @Override
    public String getId() {
        return "TutorialAuthenticatorExtension";
    }

    /**
     * Returns the name of this {@link AuthenticatorExtension}, which will be used in the UI when
     * setting up the authenticator.
     * 
     * @return the name.
     */
    @Override
    public String getName() {
        return "Tutorial Authenticator";
    }

    /**
     * Adds property controls to the wizard page of the authenticator set-up screen so that users
     * can configure properties of this authenticator.
     * 
     * @param wizardPageDefinition
     */
    @Override
    public void populateAuthenticatorWizardPage(WizardPageDefinition wizardPageDefinition) {
        PropertyGroupDefinition propertyGroup = new PropertyGroupDefinition("Admin User", 1, 10);
        PropertyDefinition nameDefinition = new PropertyDefinition(NAME, "Admin user name", PropertyType.STRING);
        PropertyDefinition pwdDefinition = new PropertyDefinition(PASSWORD, "Admin user password", PropertyType.PASSWORD);
        propertyGroup.addPropertyDefinition(nameDefinition);
        propertyGroup.addPropertyDefinition(pwdDefinition);
        wizardPageDefinition.addPropertyGroup(propertyGroup);
    }

    /**
     * Creates an {@link AbstractAuthenticator} instance.
     * 
     * @param conf
     *            The configuration used to configure the instance.
     * @return an {@link AbstractAuthenticator} instance.
     */
    @Override
    public AbstractAuthenticator createAuthenticator(GenericConfiguration conf) {
        String[] usernames = conf.getStringPropertyArray(NAME);
        String[] passwords = conf.getPasswordPropertyArray(PASSWORD);
        List<Account> exampleAccounts = new ArrayList<Account>();
        for (int i = 0; i < passwords.length; i++) {
            exampleAccounts.add(new Account(usernames[i], passwords[i]));
        }
        return new TutorialAuthenticator(exampleAccounts, Arrays.asList(new Account("john", "doe")));
    }
}

In the populateAuthenticatorWizardPage(...) method you define that up to 10 admin accounts can be configured on the authenticator set-up screen. Whatever has been set-up there persists in the Datameer database and gets passed into the createAuthenticator(...) method. We grab the usernames and passwords and use this to create and configure an ExampleAuthenticator instance.

Account.java
package datameer.das.plugin.tutorial04;

import org.apache.commons.lang.builder.*;

public class Account {
    private String _username;
    private String _password;

    public Account(String username, String password) {
        _username = username;
        _password = password;
    }

    public String getUsername() {
        return _username;
    }

    public String getPassword() {
        return _password;
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this);
    }

    @Override
    public boolean equals(Object obj) {
        return EqualsBuilder.reflectionEquals(this, obj);
    }
}

Implementing the authenticator itself is fairly simple. There are just a few methods that need to be implemented:

TutorialAuthenticator.java
package datameer.das.plugin.tutorial04;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.google.common.collect.Lists;

import datameer.dap.sdk.authentication.AbstractAuthenticator;
import datameer.dap.sdk.authentication.GenericUser;
import datameer.dap.sdk.authentication.Role;
import datameer.dap.sdk.authentication.User;

public class TutorialAuthenticator extends AbstractAuthenticator {

    private static final List<String> GROUPS = Arrays.asList("group");

    private List<Account> _adminAccounts;
    private final List<Account> _analystAccounts;

    public TutorialAuthenticator(List<Account> adminAccounts, List<Account> analystAccounts) {
        _adminAccounts = adminAccounts;
        _analystAccounts = analystAccounts;
    }

    /**
     * Authenticates user by username / password.
     *
     * @param username
     * @param password
     * @return the authenticated user or null if the user could not be authenticated.
     */
    @Override
    public User authenticateUser(String username, String password) {
        Account credentials = new Account(username, password);
        if (_adminAccounts.contains(credentials)) {
            return transformAccount(credentials, Role.ANALYST, Role.ADMIN);
        }
        if (_analystAccounts.contains(credentials)) {
            return transformAccount(credentials, Role.ANALYST);
        }
        return null;
    }

    /**
     * Lists all groups that are relevant to Datameer.
     *
     * @return all groups that are relevant to Datameer.
     */
    @Override
    public Set<String> listGroups() {
        return new HashSet<String>(GROUPS);
    }

    /**
     * Lists all users that are relevant to Datameer.
     *
     * @return all users that are relevant to Datameer.
     */
    @Override
    public List<User> listUsers() {
        List<User> users = Lists.newArrayList();
        for (Account account : _adminAccounts) {
            users.add(transformAccount(account, Role.ANALYST, Role.ADMIN));
        }
        for (Account account : _analystAccounts) {
            users.add(transformAccount(account, Role.ANALYST));
        }
        return users;
    }

    @Override
    public void testConnection() {
    }

    User transformAccount(Account account, Role... roles) {
        return new GenericUser(account.getUsername(), account.getUsername() + "@datameer.com", GROUPS, Arrays.asList(roles));
    }
}

listGroups() and listUsers() could be long running operations and could return a really large number of groups and users. Datameer is aware of this and caches the results. Therefore these two methods are only called when the cache is getting refreshed. The cache is refreshed every minute, but this could be set-up in the conf/default.properties file of Datameer.

Debugging

Add the following logging configurations into the file: <datameer-install-path>/conf/log4j-production.properties to view authentication attempts.

#file appender for external authentication 
log4j.category.org.springframework.security=DEBUG, auth

# This will prevent the auth log messages from going into the conductor.log or console
log4j.additivity.org.springframework.security=false
log4j.appender.auth=org.apache.log4j.RollingFileAppender
log4j.appender.auth.layout=org.apache.log4j.PatternLayout
log4j.appender.auth.layout.ConversionPattern=%5p [%d{yyyy-MM-dd HH:mm:ss}] - %m%n
log4j.appender.auth.File=logs/auth.log
log4j.appender.auth.MaxFileSize=10000KB
log4j.appender.auth.MaxBackupIndex=10
log4j.appender.auth.threshold=DEBUG

Source Code

This tutorial can by found in the Datameer plug-in SDK under plugin-tutorials/tutorial04.