Creating a Custom Agent Validator

This section contains information on how to write a custom Agent Validator, what it is and when it makes sense to do it.

What is an Agent Validator

An Agent Validator is a kind of plugin to the Agent, which can do custom authorization checking - it as a piece of code which handles validating permissions, group membership checking as well as retrieving available ACL or group names.

When to Create Your Own

Unless you specify anything else, dk.itp.security.passticket.DefaultValidator is used - and its implementation should be enough in most cases. If you need additional functionality, please contact us first to see if we cannot fit it into the existing concept - only in rare cases will you need to create your own validator.

Some cases might include setups where you do not have RBAC (Role-Based Access Control) because you do not have roles but need other types of permissions - e.g. some stored on a user at point of login to do simulated group checking.

What Interfaces to Implement

There are a number of interfaces where the validator can implement one or more of them; Since the agents need to work with earlier java versions, unfortunately default methods in the interfaces are not possible so instead there are different interfaces you can implement depending on the functionality you need. Our strategy has been to create additional interfaces with new methods for new functionality to ensure that all existing implementations with the existing interfaces.

At runtime, we detect which ones are implemented and thus what amount of functionality the validator has available.

These interfaces exist:

Agent Validator related interfaces
dk.itp.security.passticket.IAgentValidator // Basic functionality
dk.itp.security.passticket.IExtendedAgentValidator // Adds more information/parameters to checkPermission() and isURLAllowed() calls.
dk.itp.security.passticket.IExtendedAgentValidator2 // Adds additional information to checkPermission() calls.


Below, are the full versions:

IAgentValidator
package dk.itp.security.passticket;
/**
 * This interface must be implemented by a class that wishes to perform checking for permissions,
 * selecting default authentication type etc.
 * <p>
 * The implementation of this interface can contains checks that are specific, e.g. ACL names can be
 * required to have a certain format, depending on company policies.
 *
 * @author Kim Rasmussen
 * 
 * <pre>
 * PortalProtect - Security infrastructure
 * Copyright(c) 2001, IT Practice A/S, All rights reserved.
 * 
 * This source code is confidential.
 * </pre>
 */
public interface IAgentValidator {
	/**
	 * Checks if a user has the specified permission
	 * @throws PTException if access is denied. PTException contains reason codes: 
	 * 01 = Permission found, wrong channel, should have been HTTP, 
	 * 02 = Permission found, wrong channel, should have been SSL, 
	 * 03 = Permission found, wrong channel, should have been SCL
	 * 04 = Permission not found
	 */
	boolean checkPermission(User user, String aclName, String sessionID) throws PTException;

	/**
	 * Returns the default authentication type which will be used to login
	 */
	int getDefaultAuthType();

	/**
	 * Sets the configuration properties for the agent
	 */
	void setConfiguration(java.util.Properties props);

	/**
	 * Check if the session/user is member of a group
	 *
	 * @param sessionID The session ID to lookup
	 * @param user The user record containing information about the current session
	 * @param group The name of the group to check if the session or user belongs to
	 * @return True if the user is member og this group.
	 */
	public boolean isMemberOfGroup(User user, String group, String sessionID) throws PTException;

	/**
	 * Checks if access to the specified URL is allowed for this session
	 *
	 * @param sessionID The session ID to lookup
	 * @param user The user record containing information about the current session
	 * @param identifier The identifier of the server the user is trying to access
	 * @param url The URL to check
	 */
	public boolean isURLAllowed(User user, String identifier, String url, String sessionID) throws PTException;
	
	/**
	 * Returns the status text in html format (without &lthtml&gt tags), or null if the validator does not wish to provide any text.
	 */
	public String getStatusText();
	
	/**
	 * Return an array of acl names
	 * @return List of ACL names the user has
	 * @throws PTException
	 */
	public String[] getAclNames(User user) throws PTException;
	
	/**
	 * Return an array of group names
	 * @return List of group names the user has
	 * @throws PTException
	 */
	public String[] getGroupNames(User user) throws PTException;
}
IExtendedAgentValidator
package dk.itp.security.passticket;

/**
 * 
 * Interface for extended Agent Validators, that are able to do additional validations.
 *  
 * @author Kim Rasmussen
 * @version $Revision$
 *
 * <pre>
 * PortalProtect - Security infrastructure
 * Copyright(c) 2001, IT Practice A/S, All rights reserved.
 * 
 * This source code is confidential.
 * </pre>
 */
public interface IExtendedAgentValidator extends IAgentValidator {
	/**
	 * Checks if the user has permission to the acl / resource, taking into account the additional data delivered, e.g. for data based authorization.
	 * 
	 * @param user User/Session
	 * @param aclName ACL name to check
	 * @param sessionID Session ID
	 * @param additionalData Application specific data
	 * @return true if access is allowed, false if not
	 * @throws PTException Thrown if an error occurs
	 * @throws dk.itp.security.authorization.client.AdditionalDataRequiredException Thrown if additional data is required
	 * @throws AclNotFoundException Thrown if the specified ACL was not found 
	 */
	boolean checkPermission(User user, String aclName, String sessionID, Object additionalData) throws PTException;
	
	/**
	 * Checks if access to the specified URL is allowed for this session
	 *
	 * @param sessionID The session ID to lookup
	 * @param user The user record containing information about the current session
	 * @param identifier The identifier of the server the user is trying to access
	 * @param url The URL to check
	 * @param method The HTTP method used
	 */
	public boolean isURLAllowed(User user, String identifier, String method, String url, String sessionID) throws PTException;
}
IExtendedAgentValidator2
package dk.itp.security.passticket;

/**
 * Added even more finegraded methods to a validator - allows separating permission checks per identifier.
 *  
 * @author Kim Rasmussen
 * @version $Revision$
 *
 * <pre>
 * Ceptor - http://ceptor.io
 * 
 * This source code is confidential.
 * </pre>
 */
public interface IExtendedAgentValidator2 extends IExtendedAgentValidator {
	/**
	 * Checks if the user has permission to the acl / resource, taking into account the additional data delivered, e.g. for data based authorization.
	 * 
	 * @param agent Instance of the agent performing the check
	 * @param user User/Session
	 * @param identifier Identifier used to separate different ACL entries for different applications - use null for default
	 * @param aclName ACL name to check
	 * @param sessionID Session ID
	 * @param additionalData Application specific data
	 * @return true if access is allowed, false if not
	 * @throws PTException Thrown if an error occurs
	 * @throws dk.itp.security.authorization.client.AdditionalDataRequiredException Thrown if additional data is required
	 * @throws AclNotFoundException Thrown if the specified ACL was not found 
	 */
	boolean checkPermission(IAgent agent, User user, String identifier, String aclName, String sessionID, Object additionalData) throws PTException;

}


Example code

Below, is example code for a validator - this validator supports context-dependant authorization, and "magic" group names where environment name, authorization level etc. can be part of the group name a resource is protected by.


package dk.itp.security.passticket.samples;

import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Vector;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import dk.itp.security.authorization.client.ACLEntry;
import dk.itp.security.authorization.client.GroupEntry;
import dk.itp.security.authorization.client.PermissionEntry;
import dk.itp.security.authorization.client.URLEntry;
import dk.itp.security.authorization.policy.PolicyExecuter;
import dk.itp.security.utils.StringMatcher;
import dk.itp.security.utils.UniqueId;
import dk.itp.security.passticket.*;


public class SampleValidator implements IExtendedAgentValidator2 {
	private static Logger cat = LoggerFactory.getLogger(DefaultValidator.class.getName());
	
	private static class ParsedURLEntry {
		public String protocol;
		public String hostname;
		public String uri;		
	}
	
	private static class ProtectedUrlCacheEntry {
		URLEntry[] urls;
		ParsedURLEntry[] parsedUrls;
		long timeOfUrlRetrieval;
	}
	private Map<String, ProtectedUrlCacheEntry> namedProtectedUrls = Collections.synchronizedMap(new HashMap<String, ProtectedUrlCacheEntry>());
	
	private boolean disableEnvironmentCheck = false;
	private Hashtable<String, String> environmentAliasList = new Hashtable<String, String>();
	
	private int defaultAuthType = AuthTypes.AUTHTYPE_LDAPUIDPW;
	private boolean warnWhenAccessDenied = true;
	
	private static int URL_REFRESH_TIMER = 1000*300; // 5 minutes;

	@Override
	public boolean checkPermission(User user, String aclName, String sessionID) throws PTException {
		return checkPermission(user, aclName, sessionID, null);
	}
	
	public boolean checkPermission(User user, String aclName, String sessionID, Object additionalData) throws PTException {
		return checkPermission(null, user, null, aclName, sessionID, additionalData);
	}
	/**
	 * Checks if a user has the specified permission
	 */
	public boolean checkPermission(IAgent agent, User user, String identifier, String aclName, String sessionID, Object additionalData) throws PTException {
		int idx = aclName.lastIndexOf('.');
		if (idx <= 0) {
			cat.warn("No permission name found in ACL name, format is acl.permission");
			throw new PTException("No permission name found in ACL name, format is acl.permission");
		}

		String permission = aclName.substring(idx+1);
		aclName = aclName.substring(0, idx);

		ACLEntry acl = PTServer.getInstance().getACL(aclName, identifier);

		if (acl == null) {
			cat.debug("ACL " + aclName + " not found");
			throw new AclNotFoundException("ACL " + aclName + " not found");
		}
		
		Enumeration<PermissionEntry> enumerator = acl.getPermissions().elements();
		while (enumerator.hasMoreElements()) {
			PermissionEntry pe = enumerator.nextElement();

			if (pe.getName().equals(permission)) {
				// If there are any data policies assigned to the entry, execute them
				if (acl.getDataPolicies() != null) {
					String[] members = pe.getMembers();
					for (int i = 0; i < members.length; i++) {
						String group = members[i];
						
						boolean result = PolicyExecuter.execute(agent, user, sessionID, acl, pe, group, additionalData);
						if (result)
							return true;
					}
				// If no data policies exists, do a plain user/group check
				} else {
					String[] members = pe.getMembers();
					for (int i = 0; i < members.length; i++) {
						String name = members[i];
						
						if (members[i].equals(user.userid)) {
							return true;
						} else if (name.startsWith("uid:")) {
							if (name.substring(4).equals(user.customerID))
								return true;
						} else	if (isMemberOfGroup(user, name, sessionID)) {
							return true;
						}
					}
				}
				
				if( warnWhenAccessDenied ) {
					cat.warn("Permission " + aclName + "." + permission + " denied for session " + sessionID + ", user: " + user.userid);
				}
				else if( cat.isDebugEnabled() ) {
					cat.debug("Permission " + aclName + "." + permission + " denied for session " + sessionID + ", user: " + user.userid);
				}
				return false;
			}
		}
		return true;
	}

	/**
	 * Check if the session/user is member of a group
	 * 
	 * @param sessionID The session ID to lookup
	 * @param user The user record containing information about the current session
	 * @param groupName The name of the group to check if the session or user belongs to
	 * @return True if the user is member og this group.
	 */
	public boolean isMemberOfGroup(User user, String groupName, String sessionID) throws PTException {
	    // If the group name ends with @xx, then xx is the environment required
		int idx = groupName.lastIndexOf('@');
		if (idx > 0) {
			try {
			    // Environment checks can be disabled
			    if (!disableEnvironmentCheck) {
				    UniqueId uid = new UniqueId(sessionID);
				    
				    String checkId = groupName.substring(idx+1);
				    // Support environment alias, e.g. internet=1, or intranet=2
				    String aliasId = (String)environmentAliasList.get(checkId);
				    if (aliasId != null)
				        checkId = aliasId;
				    
					int environmentId = Integer.parseInt(checkId);
					if (uid.getEnvironmentID() != environmentId) {
						cat.warn("Access to group " + groupName + " failed, because user had environment " + uid.getEnvironmentID());
						return false;
					}
			    }
					
				groupName = groupName.substring(0, idx);
			} catch(Exception e) {
				cat.error("Invalid environment specified in check of group: " + groupName + " - access was denied");
				return false;
			}
		}
		// If the group name ends with .xx where xx is digits, it means that we should also check
		// if the user has at least authentication level xx before checking group membership.
		idx = groupName.lastIndexOf('.');
		if (idx > 0) {
			try {
				int level = Integer.parseInt(groupName.substring(idx+1));
				if (user.authenticationLevel < level) {
					cat.warn("Access to group " + groupName + " failed, because user had authentication level " + user.authenticationLevel);
					return false;
				}
					
				groupName = groupName.substring(0, idx);
			} catch(Exception e) {
				// If we get an exception, it just means that the last part of the group name
				// is not digits, so we just assume that any authentication level is ok
			}
		}
		
		// Special group that everyone is a member of
		if (groupName.equals("pp_everyone") || groupName.equals("everyone"))
			return true;
			
		// Special group that identified users are always member of
		if (groupName.equals("pp_identifiedusers"))
			return user.isLoggedOn;
			
		// Special group that anonymous users are members of
		if (groupName.equals("pp_anonymous"))
			return !user.isLoggedOn;
			
		String groups[] = user.getGroups();
		if (groups == null) {
			GroupEntry group = PTServer.getInstance().getGroup(groupName, null);
			
			if (group == null) {
				cat.warn("Group " + groupName + " not found");
//				throw new PTException("Group " + groupName + " not found");
				return false;
			}
			
			return group.getMembers().contains(user.userid);
		}
			
		for(int i = 0; i < groups.length; i++) {
			if (groupName.equals(groups[i]))
				return true;
		}
		
		return false;
	}
	
	/**
	 * Checks if access to the specified URL is allowed for this session
	 * 
	 * @param sessionID The session ID to lookup
	 * @param user The user record containing information about the current session
	 * @param identifier The identifier of the server the user is trying to access
	 * @param url The URL to check
	 */
	public boolean isURLAllowed(User user, String identifier, String url, String sessionID) throws PTException {
		return isURLAllowed(user, identifier, null, url, sessionID);
	}
	
	/**
	 * Checks if access to the specified URL is allowed for this session
	 * 
	 * @param sessionID The session ID to lookup
	 * @param user The user record containing information about the current session
	 * @param identifier The identifier of the server the user is trying to access
	 * @param url The URL to check
	 * @param method The HTTP Method
	 */
	public boolean isURLAllowed(User user, String identifier, String method, String url, String sessionID) throws PTException {
		if (identifier == null || identifier.trim().length() == 0)
			identifier = "default";
		
		// Ensure that CORS works always
		if ("OPTIONS".equalsIgnoreCase(method)) {
			if (cat.isDebugEnabled())
				cat.debug("URL check for identifier: " + identifier + ", url: " + url + ", method: " + method + " - always allowing OPTIONS method access");
			return true;
		}
		
		ProtectedUrlCacheEntry entry;
		
		synchronized (this) {
			entry = (ProtectedUrlCacheEntry) namedProtectedUrls.get(identifier);
			if (entry == null || entry.timeOfUrlRetrieval == 0 || entry.timeOfUrlRetrieval + URL_REFRESH_TIMER < System.currentTimeMillis()) {
				if (entry == null)
					entry = new ProtectedUrlCacheEntry();
				entry.urls = PTServer.getInstance().getProtectedURLs(identifier);
				if (entry.urls == null)
					entry.urls = new URLEntry[0];
				
				entry.timeOfUrlRetrieval = System.currentTimeMillis();
				
				// Preparse the URLs for faster processing
				entry.parsedUrls = new ParsedURLEntry[entry.urls.length];
				for(int i = 0; i < entry.parsedUrls.length; i++) {
					entry.parsedUrls[i] = parseUrl(entry.urls[i].getName());
				}
				namedProtectedUrls.put(identifier, entry);			
			}
		}
		
		// If no ACLs on URL entries, access is allowed
		if (entry.urls.length == 0)
			return true;
		
		boolean found = false;
		int ofs = 0;
		
		ParsedURLEntry parsedUrl = parseUrl(url);
		
		// Search for a partial match on the URL
		for (int i = 0; !found && i < entry.parsedUrls.length; i++) {
			// Check for at match
			ParsedURLEntry patternUrlEntry = entry.parsedUrls[i];
			
			if (patternUrlEntry.protocol != null && !StringMatcher.match(patternUrlEntry.protocol, parsedUrl.protocol))
				continue;
			if (patternUrlEntry.hostname != null && (parsedUrl.hostname == null || !StringMatcher.match(patternUrlEntry.hostname, parsedUrl.hostname)))
				continue;
			
			if (StringMatcher.match(patternUrlEntry.uri, parsedUrl.uri)) {
				found = true;
				ofs = i;
			}
		}
		
		// If we did not find a matching URLEntry, it means that access is allowed to all the URLs
		if (!found) {
			if (cat.isDebugEnabled())
				cat.debug("URL check for identifier: " + identifier + ", url: " + url + ", method: " + method + " - no match found, access is allowed.");
			return true;
		}
			
		if (cat.isDebugEnabled())
			cat.debug("URL check for identifier: " + identifier + ", url: " + url + ", method: " + method + " - match found to URL: " + entry.urls[ofs].getName());
		
		// If we get here, we did find a matching entry - this means that we must now check the principals to
		// verify that our user is ok
		
		Vector<String> principals = entry.urls[ofs].getPrincipals();
		
		int count = principals.size();
		for(int i = 0; i < count; i++) {
			String principal = principals.elementAt(i).toString();
			
			if (user.userid != null && user.userid.equals(principal)) {
				if (cat.isDebugEnabled())
					cat.debug("URL check for identifier: " + identifier + ", url: " + url + ", method: " + method + " - match found to URL: " + entry.urls[ofs].getName() + ", access is allowed for user: " + principal);
				return true;
			}
				
			if (isMemberOfGroup(user, principal, sessionID)) {
				if (cat.isDebugEnabled())
					cat.debug("URL check for identifier: " + identifier + ", url: " + url + ", method: " + method + " - match found to URL: " + entry.urls[ofs].getName() + ", access is allowed for group: " + principal);
				return true;
			}
		}
		
		if (cat.isDebugEnabled())
			cat.debug("URL check for identifier: " + identifier + ", url: " + url + ", method: " + method + " - match found to URL: " + entry.urls[ofs].getName() + ", access not allowed");
		
		return false;
	}
	
	/**
	 * Returns the default authentication type which will be used to login
	 */
	public int getDefaultAuthType() {
		return defaultAuthType;
	}

	/**
	 * Sets the configuration properties for the agent
	 */
	public void setConfiguration(java.util.Properties props) {
	    String str = props.getProperty("disableEnvironmentCheck", "false");
	    if (props.getProperty("defaultAuthenticationType")!=null)
	    	defaultAuthType = Integer.parseInt(props.getProperty("defaultAuthenticationType"));
	    disableEnvironmentCheck = str.equalsIgnoreCase("true");
	    warnWhenAccessDenied = new Boolean( props.getProperty("warnWhenAccessDenied", "true") ).booleanValue();

	    try {
	        Hashtable<String, String> aliasList = new Hashtable<String, String>();
		    str = props.getProperty("environmentAliasList", "");
		    StringTokenizer st = new StringTokenizer(str, ";");
		    while(st.hasMoreTokens()) {
			    StringTokenizer st1 = new StringTokenizer(st.nextToken(), "=");
			    
			    String key = st1.nextToken();
			    String value = st1.nextToken();
		        
			    aliasList.put(key, value);
		    }
		    
		    environmentAliasList = aliasList;
	    } catch(Throwable t) {
	        cat.error("Unable to parse environmentAliasList", t);
	    }
	    
	    // Force invalidation of the cached URLs
		namedProtectedUrls.clear();
	}
	
	/**
	 * @see dk.itp.security.passticket.IAgentValidator#getStatusText()
	 */
	public String getStatusText() {
		StringBuilder sb = new StringBuilder();
		sb.append("<p><b>Default Validator metrics</b>");
		sb.append("<br>&nbsp;&nbsp;defaultAuthenticationType: " + defaultAuthType);
		sb.append("<br>&nbsp;&nbsp;disableEnvironmentCheck: " + disableEnvironmentCheck);
		sb.append("</p>");
		return sb.toString();
	}

	public String[] getAclNames(User user) throws PTException {
		throw new PTException("not supported");
	}
	
	public String[] getGroupNames(User user) throws PTException {
		return user.getGroups();
	}
	
	private ParsedURLEntry parseUrl(String url) {
		url = url.toLowerCase();
		
		ParsedURLEntry entry = new ParsedURLEntry();

		// Split URL into protocol, hostname and uri
		if (url.startsWith("/")) {
			entry.uri = url;
		} else {
			int idx = url.indexOf("://");
			if (idx >= 0) {
				entry.protocol = url.substring(0, idx);
				url = url.substring(idx+3);
			}
			
			idx = url.indexOf('/');
			if (idx >= 0) {
				entry.hostname = url.substring(0, idx);
				entry.uri = url.substring(idx);
			} else {
				entry.uri = url;
			}
		}
		return entry;
	}
}