JBoss.orgCommunity Documentation

Chapter 3. Custom Membership Domains

3.1. Creating a Custom Membership Domain
3.1.1. Membership Domain API
3.1.2. File Membership Domain Example

The creation of a custom membership domain based upon the MembershipDomain interface allows for easy extensibility of the Teiid security system.   The API and associate classes are focused solely on conveying authentication and group information.  At this time other development concerns, such as access to the internal logging facilities, are not documented or generally available to custom membership domain implementors.

Within the development IDE of choice the custom membership domain developer should add the server-api.jar to project's classpath.  From there the MembershipDomain interface and the related classes can be extended to create custom membership domain.

Custom membership domains should be implemented to the specific needs of the external security system.  For example, in cases where explicit initialization or shutdown is not applicable the implementations of these methods may be left empty.

For membership domains that do require configuration information, the initialization method provides a built-in mechanism for provide a specific set of values.  Each instance of a custom membership domain defined through the Console can have a different properties file to drive its initialization.  The lookup of a properties file specified through the console will search the classpath, the filesystem, and finally as a URL in that order.  

Once the classes that represent the custom membership domain have been implemented they can be made available to a server installation by adding their jar file into the <server install root>/lib/patches directory.  If the server is part of a multi-host cluster, the jar must be added to each host running a Membership Service.

From there the Console can be used to install and configure the custom membership domain for use in the server environment.  See the Console Guide for detailed instructions.

The following section contains an example of a membership domain that uses simple text files to determine user authentication and group membership. The users are listed in a properties file containing username=password entries. The group memberships are listed in a properties file containing comma groupname=[username[,username]*] entries.

import ...

public class FileMembershipDomain implements MembershipDomain {
    public static final String USERS_FILE = "usersFile"; 
    public static final String GROUPS_FILE = "groupsFile"; 
    public static final String CHECK_PASSWORD = "checkPassword"; 

    private boolean checkPasswords;
    private Properties users;
    private HashMap groups = new HashMap();
    private HashMap userGroups = new HashMap();
    
    private Properties loadFile(String fileName) throws ServiceStateException {
        Properties result = new Properties();

        //try the classpath
        InputStream is = this.getClass().getResourceAsStream(fileName);
        
        if (is == null) {
            try {
                //try the filesystem
                is = new FileInputStream(fileName);
            } catch (FileNotFoundException err) {
                try {
                    //try a url
                    is = new URL(fileName).openStream();
                } catch (MalformedURLException err1) {
                    throw new ServiceStateException(err, "Could not load file "+fileName+" for FileMembershipDomain"); 
                } catch (IOException err1) {
                    throw new ServiceStateException(err1, "Could not load file "+fileName+" for FileMembershipDomain"); 
                }
            } 
        }
        
        try {
            result.load(is);
        } catch (IOException err) {
            throw new ServiceStateException(err, "Could not load file "+fileName+" for FileMembershipDomain"); 
        } finally {
            try {
                is.close();
            } catch (IOException err) {
            }
        }
        return result;
    }
    
    public void shutdown() throws ServiceStateException {
    }

.... <covered in the next pages> ...

}      
      

The above snippet is shows a class that implements the MembershipDomain interface. In this example no meaningful is performed in the shutdown method, so its implementation is left empty. The loadFile method is a simple utility method that will be used to load the users and groups files during initialization (shown in the next snippet).

public void initialize(Properties env) throws ServiceStateException {
    checkPasswords = Boolean.valueOf(env.getProperty(CHECK_PASSWORD,   Boolean.TRUE.toString())).booleanValue();
    
    String userFile = env.getProperty(USERS_FILE);
    String groupFile = env.getProperty(GROUPS_FILE);
    
    if (userFile == null) {
        throw new ServiceStateException("Required property " +USERS_FILE+ " was missing."); 
    }
    
    users = loadFile(userFile);
    
    if (groupFile == null) {
        throw new ServiceStateException("Required property " +GROUPS_FILE+ " was missing."); 
    }
    
    groups.clear();
    groups.putAll(loadFile(groupFile));
    userGroups.clear();
    for (Iterator i = groups.entrySet().iterator(); i.hasNext();) {
        Map.Entry entry = (Map.Entry)i.next();
        String group = (String)entry.getKey();
        String userNames = (String)entry.getValue();
        String[] groupUsers = userNames.split(","); //$NON-NLS-1$
        
        for (int j = 0; j < groupUsers.length; j++) {
            String user = groupUsers[j].trim();
            Set uGroups = (Set)userGroups.get(user);
            if (uGroups == null) {
                uGroups = new HashSet();
                userGroups.put(user, uGroups);
            }
            uGroups.add(group);
        }
    }
}      
      

The initialize method is written to expect two properties “usersFile” and “groupsFile”. Values for these properties should be defined in the properties file specified in the “properties file” connector binding. An optional property “checkPasswords” may be specified that will determine if the membership domain should check the credentials specified as passwords.

public SuccessfulAuthenticationToken authenticateUser(String username, Credentials credential,Serializable trustedPayload, String applicationName) 
  throws UnsupportedCredentialException,InvalidUserException,LogonException,MembershipSourceException {
    if (username == null || credential == null) {
        throw new UnsupportedCredentialException("a username and password must be supplied for this domain"); 
    }
    
    String password = (String)users.get(username);
    
    if (password == null) {
        throw new InvalidUserException("user " + username + " is invalid"); 
    }
    
    if (!checkPasswords || password.equals(String.valueOf(credential.getCredentialsAsCharArray()))) {
        return new SuccessfulAuthenticationToken(trustedPayload, username);
    }
                            
    throw new LogonException("user " + username + " could not be authenticated"); 
}
      

The authenticateUser method implementation demonstrates several possible outcomes for an authentication attempt.  If a user name and password (in the form of a Credentials object) are not supplied the domain will indicate that it does not support the authentication attempt.  In this case for unqualified logons, authentication would proceed to the next membership domain.

If a password cannot be found for the user name cannot be found in the users file, then the domain reports that the user is not valid for the current domain.  As with the previous case, unqualified logons would proceed to the next membership domain.  

If the domain is not checking passwords or the password value matches the supplied credentials, the membership domain returns a SuccessfulAuthenticationToken.  This token may contain an augmented value of the trustedPayload, however in this example the value that was passed in is returned unchanged.

Finally if authentication was not successful, a LogonException is thrown to indicate the user has failed authentication.  Authentication failure due to a LogonException will immediately fail the overall user authentication even with an unqualified logon.

NOTE: The message text, and any chained exceptions, in an UnsupportedCredentialException,  InvalidUserException, or LogonException will appear in server log.

public Set getGroupNames() throws MembershipSourceException {
  Set resultNames = new HashSet(groups.keySet());
    return resultNames;
}

public Set getGroupNamesForUser(String username) throws InvalidUserException,
                                                MembershipSourceException {
  // See if this user is in the domain
    if (!users.containsKey(username)) {
        throw new InvalidUserException("user " + username + " is invalid"); 
    }

    Set usersGroups = (Set)userGroups.get(username);
    if (usersGroups == null) {
        return Collections.EMPTY_SET;
    }
    return usersGroups;
}      
      

The last two methods needed to implement the MembershipDomain interface are shown above.  The getGroupNames  method returns all known group names from the groups file.  The getGroupNamesForUser method returns all groups for the given user.  The mapping from users to groups was established in the initialize method.

NOTE: It is important that the return values from all of the MembershipDomain methods are Serializable

NOTE:  The preceding example is case sensitive with respect to user names.  The Teiid system does not require logons to be case insensitive.  That is up to the implementation of the member domain.