Jira User Directory Custom Attributes Integration

18 Jun, 2018 | 5 minutes read

Jira supports integration with external LDAP User Directories. This is a powerful feature that allows authentication and authorization of users registered into existing (corporate) directories. One of the LDAP templates supported out of the box is Microsoft Active Directory. LDAP User Directory integration is particularly useful in the case of Jira Service Desk where users shall be able to access the Service Desk portal and nothing else (they are not assigned to any group associated with the Jira application: jira-core-users, jira-software-users, jira-servicedesk-users). The licensing mechanism does not apply to these users, they are Service Desk Customers only.

At the same time, LDAP User Directory synchronizes and maps into the Jira’s internal user model only a couple of attributes out of many. For example, Microsoft Active Directory stores user’s telephone number, addresses, certificates etc. All of these attributes are not available in Jira by default. Imagine help desk application where the Customer’s telephone numbermail or address are available to the Agent. This way the Agent will be able to contact the Customer that raised the ticket using alternative channels (phone for example).

This article outlines the creation of Jira Workflow Post Function that will enhance the Service Desk ticket description with additional information automatically attached at the bottom of the ticket description. For example, the description of the ticket:

I have a problem with the VPN access.

Becomes:


I have a problem with the VPN access.

- - - - - - - - - - - - - - - - - - - - - - 

Mail: john.smith@example.com

Telephone Number: +1 123 456 7890

- - - - - - - - - - - - - - - - - - - - - -

The Post function will be configured with only one attribute ldapAttributes. The format of this multi-line attribute should match the Java Properties format in the form descriptionLabel=ldapAttribute (key=value). For example:

Name=displayName

User\u0020Mail=mail

Phone\u0020Number=telephoneNumber

Note that the white spaces must be escaped with appropriate Unicode code (that’s how Java Properties work).

The Velocity templates for presenting and editing the LDAP attributes to be fetched are:

fetch-user-attributes-function-input.vm

<div>
       <u>LDAP Attributes</u>
</div>

<div>
      <textarea rows="20" cols="120" id="ldapAttributes" name="ldapAttributes">$ldapAttributes</textarea>

fetch-user-attributes-function.vm

<div>
       <u>LDAP Attributes</u>
</div>

<div>
        <pre>$ldapAttributes</pre>
</div>

The logic for retrieving additional user attributes is based on the currently logged in user. The user is always associated with an existing Jira User Directory he/she belongs to. We can obtain the access information for that directory (URL, credentials, security protocol etc.) from the class DirectoryManager. Once we have that directory access info we can fetch the requested LDAP attributes for the current user. These attributes are available for injection into various ticket/task fields after that. In this particular example, we are constructing contact information that will enhance the ticket description with “signature” at the end of the text.

FetchUserAttributesFunction.java

package com.interworks.jira.plugins.workflow;

import java.io.StringReader;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

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

import com.atlassian.crowd.embedded.api.Directory;
import com.atlassian.crowd.manager.directory.DirectoryManager;
import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.issue.MutableIssue;
import com.atlassian.jira.security.JiraAuthenticationContext;
import com.atlassian.jira.user.ApplicationUser;
import com.atlassian.jira.workflow.function.issue.AbstractJiraFunctionProvider;
import com.opensymphony.module.propertyset.PropertySet;
import com.opensymphony.workflow.WorkflowException;

public class FetchUserAttributesFunction extends AbstractJiraFunctionProvider {

	private static final Logger log = LoggerFactory.getLogger(FetchUserAttributesFunction.class);

	public static final String FIELD_LDAP_ATTRIBUTES = "ldapAttributes";

	@SuppressWarnings("rawtypes")
	public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException {
		MutableIssue issue = getIssue(transientVars);

		DirContext ldapContext = null;

		String ldapAttributes = (String) args.get(FIELD_LDAP_ATTRIBUTES);

		try {
			if (ldapAttributes == null)
				return;

			Properties pLdapAttributes = new Properties();

			pLdapAttributes.load(new StringReader(ldapAttributes));

			DirectoryManager directoryManager = ComponentAccessor.getComponent(DirectoryManager.class);
			JiraAuthenticationContext authContext = ComponentAccessor.getJiraAuthenticationContext();
			ApplicationUser user = authContext.getLoggedInUser();
			String username = user.getUsername();
			long directoryID = user.getDirectoryId();
			Directory directory = directoryManager.findDirectoryById(directoryID);
			Map<String, String> directoryAttributes = directory.getAttributes();

			String ldapUrl = directoryAttributes.get("ldap.url");
			String ldapUser = directoryAttributes.get("ldap.userdn");
			String ldapPassword = directoryAttributes.get("ldap.password");
			String ldapUserFilter = directoryAttributes.get("ldap.user.filter");
			String baseDn = directoryAttributes.get("ldap.basedn");
			String ldapSecure = directoryAttributes.get("ldap.secure");

			if (ldapUrl == null)
				return;

			Hashtable<String, String> ldapEnv = new Hashtable<String, String>(11);
			ldapEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
			ldapEnv.put(Context.PROVIDER_URL, ldapUrl);
			ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
			ldapEnv.put(Context.SECURITY_PRINCIPAL, ldapUser);
			ldapEnv.put(Context.SECURITY_CREDENTIALS, ldapPassword);
			if ("true".equalsIgnoreCase(ldapSecure))
				ldapEnv.put(Context.SECURITY_PROTOCOL, "ssl");

			ldapContext = new InitialDirContext(ldapEnv);
			SearchControls searchCtls = new SearchControls();
			searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
			String searchFilter = ldapUserFilter.replaceAll("\\*", username);

			log.debug("Search Filter:" + searchFilter);
			NamingEnumeration<SearchResult> answer = ldapContext.search(baseDn, searchFilter, searchCtls);
			if (answer.hasMoreElements()) {
				SearchResult searchResult = (SearchResult) answer.next();
				Attributes attributes = searchResult.getAttributes();
				StringBuilder contactInfo = new StringBuilder();
				contactInfo.append("\n-------------------------------------------------------\n");
				Set<String> attributeLabels = pLdapAttributes.stringPropertyNames();
				for (String attributeLabel : attributeLabels) {
					String attributeName = pLdapAttributes.getProperty(attributeLabel);
					String attributeValue = getAttributeValue(attributes, attributeName);
					contactInfo.append(attributeLabel).append(":").append(attributeValue).append("\n");
				}
				contactInfo.append("-------------------------------------------------------\n");
				issue.setDescription(issue.getDescription() + contactInfo.toString());

			} else {
				log.debug("No LDAP user found for:" + username);
			}

		} catch (Throwable t) {
			throw new RuntimeException(t);
		} finally {
			if (ldapContext != null)
				try {
					ldapContext.close();
				} catch (NamingException e) {
					log.error(e.getMessage(), e);
				}
		}
	}

	private String getAttributeValue(Attributes attributes, String name) throws NamingException {
		Attribute attribute = attributes.get(name);
		if (attribute != null) {
			Object attributeValue = attribute.get();
			if (attributeValue != null)
				return attributeValue.toString();
		}
		return "";
	}
}

The created Workflow Post function shall be attached on the Create transition of the workflow associated to the ticket/task type.

Besides this specialized plugin, the same functionality can be accomplished with Groovy scripting. I’ve already written an article on this topic:

With this scripting plugin the code shown above can be adapted with minimal changes into Groovy script that will do the same. In addition, besides ticket description enrichment, the Groovy script can store the LDAP attributes into some ticket/task custom fields for easy search and manipulation.

Jira Plugins and Workflow Post functions are a powerful way to enhance Jira. This is just one example that will add value to the Jira Service Desk functionality. InterWorks’ Jira consultants listen to the problems and requirements of their clients. And… usually we don’t see constraints to implement some requirement or solve the problem because our Jira solutions portfolio covers many use cases solved in a way similar to what was explained in this article.

Cheers!