Website Downloads Documentation Knowledgebase Wiki Issue tracker Commercial support

Creating a multi role authentication scheme

This step by step instruction explains how to extend the Daisy CMS with your own authentication scheme that fits your needs.

Assumption: You already set up daisy on a windows system and everything's working great. Your also doing the codeing and compiling on a separate system (a mac) because you too lazy to install the dev tool on the windows server...

Task: You would like new daisy users to be allocated a given role when they first logon. The system admin chaps would ideally like the group the user belongs to on a windows network to dictate this role allocation.

This solution is useful where there are a number of different users who need to see different resources on the same server (ie. student and staff at a university).

Step 1: Checking out the sources

Download the daisy sources daisy-x.x.x.tar.gz of the latest stable (or milestone) release at the download area (or check out the latest version from SVN, whatever you prefer). Extract the tarball into a directory on your local machine (although you do not have to create that environment variable, I will refer to that directory as DAISY_SRC during the rest ot this tutorial):

$ tar xvzf daisy-x.x.x.tar.gz

Step 2: Copy the sources of the Ntlm-authentication

Descend to the dircectory $DAISY_SRC/services/ and make a copy of the directory ntlm-auth, which contains the sources for the Ntlm authentication scheme. We take these sources as starting point and we will modify them to fit our needs.

$ cp -R ntlm-auth/ ntlm-group-auth

Step 3: Create the java classes for authentication

Step 3a: Rename the existing sourcefiles for AuthenticationFactory and AuthenticationScheme

Descend into the directory $DAISY_SRC/services/ssh-auth/src/java/org/outerj/daisy/authentication/impl
Rename the both existing sourcefiles for the AuthenticationFactory and the AuthenticationScheme

$ mv NtlmAuthenticationFactory.java NtlmGroupAuthenticationFactory.java
$ mv NtlmAuthenticationScheme.java NtlmGroupAuthenticationScheme.java

Step 3b: Edit NTLMGroupAuthenticationFactory.java

In your favourite editor or IDE, edit the source file for the NTLM Group Authentication Factory and change it to:

package org.outerj.daisy.authentication.impl;

import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.outerj.daisy.authentication.spi.*;
import org.outerj.daisy.plugin.PluginRegistry;

import javax.annotation.PreDestroy;
import java.util.Map;
import java.util.HashMap;

/**
 * Constructs and registers NtlmGroupAuthenticationSchemes with the UserAuthenticator.
 *
 */
public class NtlmGroupAuthenticationFactory  {
    private PluginRegistry pluginRegistry;
    private Map<String, AuthenticationScheme> schemes = new HashMap<String, AuthenticationScheme>();

    public NtlmGroupAuthenticationFactory(Configuration configuration, PluginRegistry pluginRegistry) throws Exception {
        this.pluginRegistry = pluginRegistry;
        this.configure(configuration);
        registerSchemes();
    }

    @PreDestroy
    public void destroy() {
        unregisterSchemes();
    }

    private void registerSchemes() throws Exception {
        for (Map.Entry<String, AuthenticationScheme> entry : schemes.entrySet()) {
            pluginRegistry.addPlugin(AuthenticationScheme.class, entry.getKey(), entry.getValue());
        }
    }

    private void unregisterSchemes() {
        for (Map.Entry<String, AuthenticationScheme> entry : schemes.entrySet()) {
            pluginRegistry.removePlugin(AuthenticationScheme.class, entry.getKey(), entry.getValue());
        }
    }

    private void configure(Configuration configuration) throws ConfigurationException {
        Configuration[] schemeConfs = configuration.getChildren("scheme");
        for (Configuration schemeConf : schemeConfs) {
            String name = schemeConf.getAttribute("name");
            String description = schemeConf.getAttribute("description");
            String domainControllerAddress = schemeConf.getChild("domainControllerAddress").getValue();
            String domain = schemeConf.getChild("domain").getValue();
			
			String file1 = schemeConf.getChild("fileLocation1").getValue();
			String file2 = schemeConf.getChild("fileLocation2").getValue();
			
			Configuration subUserCreator1 = schemeConf.getChild("subUserCreator1");
			UserCreator userCreator1 = UserCreatorFactory.createUser(subUserCreator1, name);
			
			
			Configuration subUserCreator2 = schemeConf.getChild("subUserCreator2");			
			UserCreator userCreator2 = UserCreatorFactory.createUser(subUserCreator2, name);

		
			AuthenticationScheme scheme = new NtlmGroupAuthenticationScheme(name, description, domainControllerAddress, domain, file1, userCreator1, file2, userCreator2);
			Configuration cacheConf = schemeConf.getChild("cache");
			if (cacheConf.getAttributeAsBoolean("enabled")) {
				int maxCacheSize = cacheConf.getAttributeAsInteger("maxCacheSize", 3000);
				long maxCacheDuration = cacheConf.getAttributeAsLong("maxCacheDuration", 30 * 60 * 1000); // default: half an hour
				scheme = new CachingAuthenticationScheme(scheme, maxCacheDuration, maxCacheSize);
			}

			if (schemes.containsKey(name))
				throw new ConfigurationException("Duplicate authentication scheme name: " + name);
			
			schemes.put(name, scheme);
				

            
        }
    }
}

Note that this authentication factory reads in the configuration for two files and userCreators which are then passed to the authentication scheme.

Step 3c: Edit NTLMGroupAuthenticationScheme.java

In your favourite editor or IDE, edit the source file for the NTLM Group Authentication Scheme and change it to:

package org.outerj.daisy.authentication.impl;

import org.outerj.daisy.authentication.spi.AuthenticationScheme;
import org.outerj.daisy.authentication.spi.AuthenticationException;
import org.outerj.daisy.authentication.spi.UserCreator;
import org.outerj.daisy.repository.Credentials;
import org.outerj.daisy.repository.user.User;
import org.outerj.daisy.repository.user.UserManager;
import jcifs.smb.SmbException;
import jcifs.smb.SmbAuthException;
import jcifs.smb.SmbSession;
import jcifs.smb.NtlmPasswordAuthentication;
import jcifs.smb.*;
import jcifs.UniAddress;

import java.net.UnknownHostException;
import java.net.MalformedURLException;

public class NtlmGroupAuthenticationScheme implements AuthenticationScheme {
    private final String name;
    private final String description;
    private final String domainControllerAddress;
    private final String domain;
	private final String file1;
    private final UserCreator userCreator1;
	private final String file2;
    private final UserCreator userCreator2;

    public NtlmGroupAuthenticationScheme(String name, String description, String domainControllerAddress, String domain, String file1, UserCreator userCreator1, String file2, UserCreator userCreator2) {
        this.name = name;
        this.description = description;
        this.domainControllerAddress = domainControllerAddress;
        this.domain = domain;
		this.file1 = file1;
		this.userCreator1 = userCreator1;
		this.file2 = file2;
        this.userCreator2 = userCreator2;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }

    public boolean check(Credentials credentials) throws AuthenticationException {
		if ( check1(credentials) ) {
			return true;
		}
		if ( check2(credentials) ) {
			return true;
		}			
		return false;
    }

    public boolean check1(Credentials credentials) throws AuthenticationException {
        UniAddress mydomaincontroller;
        try {
            mydomaincontroller = UniAddress.getByName(domainControllerAddress);
        } catch (UnknownHostException e) {
            throw new AuthenticationException("Error authenticating using NTLM.", e);
        }
        NtlmPasswordAuthentication mycreds = new NtlmPasswordAuthentication(domain, credentials.getLogin(), credentials.getPassword());		
		jcifs.Config.registerSmbURLHandler();
		SmbFile smbf;
		try {
			smbf = new SmbFile(file1, mycreds);

		}	catch( MalformedURLException seed ) {
            // NETWORK PROBLEMS?
            throw new AuthenticationException("Can't find the file specified", seed);
        }

        try {			
            return smbf.exists();	
        } catch( SmbAuthException sae ) {
            // AUTHENTICATION FAILURE
            return false;
        } catch( SmbException se ) {
            // NETWORK PROBLEMS?
            throw new AuthenticationException("Error authenticating using NTLM.", se);
        }
    }

    public boolean check2(Credentials credentials) throws AuthenticationException {
        UniAddress mydomaincontroller;
        try {
            mydomaincontroller = UniAddress.getByName(domainControllerAddress);
        } catch (UnknownHostException e) {
            throw new AuthenticationException("Error authenticating using NTLM.", e);
        }
        NtlmPasswordAuthentication mycreds = new NtlmPasswordAuthentication(domain, credentials.getLogin(), credentials.getPassword());
		
		jcifs.Config.registerSmbURLHandler();
		
		SmbFile smbf;
		
		try {
			smbf = new SmbFile(file2, mycreds);
		}	catch( MalformedURLException seed ) {
            // NETWORK PROBLEMS?
            throw new AuthenticationException("Can't find the file specified", seed);
        }

        try {
									
			return smbf.exists();
			
        } catch( SmbAuthException sae ) {
            // AUTHENTICATION FAILURE
            return false;
        } catch( SmbException se ) {
            // NETWORK PROBLEMS?
            throw new AuthenticationException("Error authenticating using NTLM.", se);
        }
    }

    public void clearCaches() {
        // do nothing
    }

    public User createUser(Credentials crendentials, UserManager userManager) throws AuthenticationException {
        if (userCreator1 != null) {
		if (check1(crendentials)) {
				User aUser = userCreator1.create(crendentials.getLogin(), userManager);
				return aUser;				
			}
        }
		if (userCreator2 != null) {
			if (check2(crendentials)) {
				User aUser = userCreator2.create(crendentials.getLogin(), userManager);
				return aUser;					
			}
        }
        return null;
    }
}

The jcifs library which daisy uses to perform NTLM authentication does not provide a simple method for checking if a user belongs to a certain windows group. However, by using the SmbFile() method for the jcifs library we can see if a user with a given set of creidentials can access a file on a windows share. As a windows server can restrict access using groups this provides a cunning workaround.

The location of the file and other parameters are read from a the configuration file myconfig.xml, which we will create later on. This information is used inside the constructor of a new instance of our SSHAuthenticationScheme, which we created above.

You'll notice this section of code contains a number of fairly similar methods [i.e. check1() and check2()]. The methods which Daisy itself uses  [i.e. check() and createUser()] calls these in order to perform the authentication at the two different levels .

I'm sure there a more elegent way of doing this....

Step 4: Building the authentication scheme

Step 4a: Compiling and packing your classes

I compile the files on my mac something like this:

Use the method of your choice to compile the code (Ant, Maven, your IDE, ...). When using the command below, we hope you know enough about this that everything should be on one line. For Windows, replace $DAISY_HOME with %DAISY_HOME% and the colons with semicolons)

javac -classpath 
   ~/Documents/intranet/daisy-2.2/lib/daisy/jars/daisy-repository-api-2.2.jar:
   ~/Documents/intranet/daisy-2.2/lib/daisy/jars/daisy-repository-server-spi-2.2.jar:
   ~/Documents/intranet/daisy-2.2/lib/daisy/jars/daisy-pluginregistry-api-2.2.jar:
   ~/Documents/intranet/daisy-2.2/lib/daisy/jars/daisy-pluginregistry-api-2.2.jar:
   ~/Documents/intranet/daisy-2.2/lib/avalon-framework/jars/avalon-framework-api-4.3.jar:
   ~/Documents/intranet/daisy-2.2/lib/jcifs/jars/jcifs-1.1.11.jar:
   ~/Documents/intranet/daisy-2.2/lib/javax.annotation/jars/jsr250-api-1.0.jar:
   ~/Documents/intranet/daisy-2.2/lib/javax.servlet/jars/servlet-api-2.4.jar:
   ~/Documents/intranet/ntlm-group-auth/src/org/outerj/daisy/authentication/impl/NtlmGroupAuthenticationScheme.class 
   ~/Documents/intranet/ntlm-group-auth/src/org/outerj/daisy/authentication/impl/NtlmGroupAuthenticationFactory.java 
   ~/Documents/intranet/ntlm-group-auth/src/org/outerj/daisy/authentication/impl/NtlmGroupAuthenticationScheme.java

jar cvf ntlm-group-auth.jar *

I run this command from the directory shown below:

~/Documents/intranet/ntlm-group-auth/src/

Step 4b: Copying the jar-file into place

Copy the newly created jar file both into the lib directory of your daisy binary distribution ($DAISY_HOME/lib). Copy the file daisy-auth-ssh-<version>.jar into the directory daisy/jars/.

$ cp target/daisy-auth-ssh-<version>.jar $DAISY_HOME/lib/daisy/jars/

If you don't want to create a binary distribution of daisy it might be sufficient to copy the jar-file to the $DAISY_HOME/lib directory only.

Details from this point on are a little sketchy, need to check the info on a different PC.

Step 5: Registering and configuring the new component

Step 5a: Registering the new component

Edit the file $DAISY_HOME/repository-server/conf/block.xml. Inside the container named authentication, include your newly created scheme. The element <container> of block.xml now should look like (adapt the versions of your jar files for ntlm and ssh authentication!):

<container name="authentication">
  <services>
    <service type="org.outerj.daisy.authentication.UserAuthenticator">
      <source>authenticator</source>
    </service>
    <service type="org.outerj.daisy.authentication.AuthenticationSchemeRegistrar">
      <source>authenticator</source>
    </service>
  </services>

  <component name="authenticator" class="org.outerj.daisy.authentication.impl.UserAuthenticatorImpl"/>

  <component name="daisy-native" class="org.outerj.daisy.authentication.impl.DaisyAuthenticationFactory">
    <configuration>
      <cache enabled="true" maxCacheSize="3000" maxCacheDuration="1800000"/>
    </configuration>
  </component>

  <component name="ldap" class="org.outerj.daisy.authentication.impl.LdapAuthenticationFactory">
    <configuration>
      <!-- See myconfig.xml.template for an example configuration -->
    </configuration>
  </component>

  <include name="ntlm" id="daisy:daisy-auth-ntlm" version="1.4-dev"/>
  <include name="ssh" id="daisy:daisy-auth-ssh" version="1.4-dev"/>
  <include name="ntlm-group" id="daisy:daisy-auth-ssh" version="1.4-dev"/>

</container>

Step 5b: Configuring the new component

Inside the directory $DAISY_HOME/repository-server/conf/ you will find a file myconfig.xml.template. Copy this file into the directory conf/ inside your repository and rename it to myconfig.xml

$ cp $DAISY_HOME/repository-server/conf/myconfig.xml.template /path/to/your/repository/conf/myconfig.xml

Edit the newly created file myconfig.xml. Add a target in order to configure your authentication scheme, inside that target, fill in the IP or host name of your windows server. Also, in order to enable automatically creation of user accounts once the user logs in to daisy for the first time, define our newly created scheme as authentification scheme for user creation as shown below:

<targets>
  <target path="..."
    ...
  </target>

  ... many more targets

  <target path="/daisy/repository/authentication/authenticator">
    <configuration>
      <!-- Indicates which authentication scheme to use, if any, to automatically create new users. -->
      <authenticationSchemeForUserCreation>ntlmgroup1</authenticationSchemeForUserCreation>
    </configuration>
  </target>

  ... many more targets

  <target path="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX">
    <configuration>
       <!-- You can configure one or more NTLM-based authentication schemes here -->
          <!-- Notes:
                - the name of a scheme should not be daisy, no two schemes can have the same name
                - the autoCreateUser element is optional
          -->
          <scheme name="ntlmgroup1" description="Test NTLM Group config">
            <domainControllerAddress>127.0.0.1</domainControllerAddress>
            <domain>yum</domain>
            <cache enabled="true" maxCacheSize="3000" maxCacheDuration="1800000"/>
			<fileLocation1>afilelocation</fileLocation1>
			<fileLocation2>afilelocation</fileLocation2>
			<subUserCreator1>
			   <autoCreateUser>
				  <roles>
					<role>User</role>
				  </roles>
				  <defaultRole>User</defaultRole>
				  <updateableByUser>true</updateableByUser>
				</autoCreateUser>
				<test>astring</test>
                         </subUserCreator1>
			 <subUserCreator2>
			   <autoCreateUser>
				  <roles>
					<role>User</role>
				  </roles>
				  <defaultRole>User</defaultRole>
				  <updateableByUser>true</updateableByUser>
				</autoCreateUser>
                         </subUserCreator2>
	</scheme>
    </configuration>
  </target>
</targets>

Step 6: Running and testing the new scheme

Now we are ready to test our new scheme! Restart the repository server and the daisy wiki.
Try to log on to daisy with any username/password combination which should have permissions to access one of the files at either FileLocation1 or FileLocation2.

Log in should be successfull, you should be logged on with the role defined in the myconfig.xml configuration file. Login as an administrator, invoke the user administration. In the user list, an account should exist with the username you just logged on as. Also, if you create a new user or edit an existing user, in the drop-down box for the authentication scheme, you now should have the choice between the daisy built in scheme and the scheme we newly created.

It's worth noting a couple of points:

  • if a user can access both files at FileLocation1 and FileLocation2 they don't get both sets of roles
  • the roles associated with FileLocation1 and subUserCreator1 are checked before  FileLocation2 and subUserCreator1

Best of luck getting it to work. A working example is provided here (application/octet-stream, 11.4 kB, info).

Java's not my strong point; I'd never coded in it before tying this. I'm sure that there's a better way of structuring this code, possibly involving passing arrays of FileLocations and subUserCreators from the authentication factory to the authentication scheme. Please leave some hints. :)

Comments (0)
Advertisement

Daisy hosting, installation, support. Workshops and turnkey Daisy CMS projects. Get Daisy from its creators.

outerthought.org

Downloads provided by

SourceForge.net Logo

Open source stats