- PVSM.RU - https://www.pvsm.ru -
Встала задача интеграции JIRA 4.1 с Active Directory со следующими условиями:
И вот как я её решил:
Топик опубликован по просьбеина XoJIoD [1] — так что плюсы ему.
В JIRA 4.1 есть готовый механизм интеграции с LDAP, но единственное, что он умеет — делать проверку паролей, т.е. ни о какой синхронизации речи не идет. Данный шаг можно пропустить, т.к. в дальнейшем мы доверим проверку паролей SPNEGO.
Итак, заходим в JIRA под учетной записью администратора и переходим в раздел Administration → System → LDAP и заполняем форму:

, где:
Если появилось сообщение об ошибке PartialResultException, попробуйте использовать порт Global Catalog (3268). Если всё заполнено правильно, должно появиться сообщение LDAP Authentication successful и, чуть ниже, содержимое XML-файла для интеграции. Скопируйте его, остановите сервер JIRA и замените этим содержимым то, что было в файле $JIRA_HOME/atlassian-jira/WEB-INF/classes/osuser.xml. Пользуясь случаем также включим выполнение Jelly-скриптов, для этого в файле $JIRA_HOME/bin/setenv.sh (для Linux) добавим в параметр JAVA_OPTS строку -Djira.jelly.on=true.
После этого вновь запустите JIRA и убедитесь, что для пользователей, которые есть в AD, используются пароли оттуда.
Программу для генерации скрипта, создающего пользователей и группы, я написал в самом начале, а потом жалко было выкидывать. Поэтому, если добавить в аутентификатор или службу (см. ниже) создание групп, этот шаг также можно пропустить.
В документации на сайте Atlassian была найдена утилита [2] с открытым исходным кодом, которая генерирует Jelly-скрипт, создающий пользователей LDAP в JIRA. Однако, мне также нужно было создавать группы, поэтому, вооружившись документацией по Jelly-тегам [3], дописал эту утилиту до такого состояния:
import java.io.File;
import java.io.FileReader;
import java.util.Properties;
import java.util.Random;
import javax.naming.NamingEnumeration;
import javax.naming.directory.*;
import org.apache.commons.lang3.StringEscapeUtils;
public class LDAPImporter {
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
File path = new File(LDAPImporter.class.getProtectionDomain()
.getCodeSource().getLocation().toURI());
if (!path.isDirectory()) {
path = path.getParentFile();
}
properties.load(new FileReader(new File(path, "LDAP.properties")));
properties.put("java.naming.factory.initial",
"com.sun.jndi.ldap.LdapCtxFactory");
DirContext groupContext = new InitialDirContext(properties);
SearchControls groupControls = new SearchControls();
groupControls.setReturningAttributes(new String[] {
(String) properties.get("groupNameAttribute"),
(String) properties.get("groupDNAttribute") });
groupControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
System.out.println("<JiraJelly xmlns:jira="jelly:com.atlassian.jira.jelly.JiraTagLib">n");
NamingEnumeration results = groupContext.search(
(String) properties.get("baseDN"),
(String) properties.get("groupFilter"), groupControls);
// Цикл по группам (Organisation Unit'ам)
while (results != null && results.hasMoreElements()) {
SearchResult result = (SearchResult) results.next();
Attribute attribute = result.getAttributes().get(
(String) properties.get("groupNameAttribute"));
if (attribute == null) {
continue;
}
String groupName = (String) attribute.get();
String groupDN = (String) result.getAttributes()
.get((String) properties.get("groupDNAttribute")).get();
// Создание группы
System.out.println("<jira:CreateGroup group-name=""
+ StringEscapeUtils.escapeXml("[AD] " + groupName)
+ ""/>n");
DirContext userContext = new InitialDirContext(properties);
SearchControls userControls = new SearchControls();
userControls.setReturningAttributes(new String[] {
(String) properties.get("userLoginAttribute"),
(String) properties.get("userNameAttribute"),
(String) properties.get("userMailAttribute") });
userControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
NamingEnumeration users = userContext.search(groupDN,
(String) properties.get("userFilter"), userControls);
// Цикл по пользователям в группе
while (users != null && users.hasMoreElements()) {
SearchResult user = (SearchResult) users.next();
attribute = user.getAttributes().get(
(String) properties.get("userLoginAttribute"));
if (attribute == null) {
continue;
}
String userLogin = (String) attribute.get();
attribute = user.getAttributes().get(
(String) properties.get("userNameAttribute"));
String userName;
if (attribute != null) {
userName = (String) attribute.get();
} else {
userName = userLogin;
}
attribute = user.getAttributes().get(
(String) properties.get("userMailAttribute"));
String userMail;
if (attribute != null) {
userMail = (String) attribute.get();
} else {
userMail = userLogin.replace(' ', '.') + '@'
+ properties.get("userMailDomain");
}
/*
* Т.к. будут использоваться пароли из AD,
* генерируем случайный пароль
*/
String password = Integer.toHexString(new Random().nextInt());
// Создание пользователя
System.out.println("<jira:CreateUser username=""
+ StringEscapeUtils.escapeXml(userLogin.toLowerCase())
+ "" password="" + password + "" confirm=""
+ password + "" fullname=""
+ StringEscapeUtils.escapeXml(userName) + "" email=""
+ StringEscapeUtils.escapeXml(userMail) + ""/>n");
// Добавление пользователя в группу
System.out.println("<jira:AddUserToGroup username=""
+ StringEscapeUtils.escapeXml(userLogin.toLowerCase())
+ "" group-name=""
+ StringEscapeUtils.escapeXml("[AD] " + groupName)
+ ""/>n");
}
}
System.out.println("</JiraJelly>");
}
}
Настройки берутся из файла LDAP.properties, находящемся в одной директории с программой, выглядит он примерно так:
# Адрес сервера LDAP
java.naming.provider.url=ldap://127.0.0.1:389
# DN пользователя для подключения
java.naming.security.principal=cn=test,ou=users,dc=local,dc=domain
# Пароль пользователя
java.naming.security.credentials=password
# Корневой DN
baseDN=ou=users,dc=local,dc=domain
# Атрибут с именем группы
groupNameAttribute=name
# Атрибут с DN группы
groupDNAttribute=distinguishedName
# Фильтр групп
groupFilter=(objectclass=organizationalUnit)
# Атрибут с логином пользователя
userLoginAttribute=mail
# Атрибут с именем пользователя (если не указано, равно логину)
userNameAttribute=displayName
# Атрибут с адресом e-mail пользователя (если не указано, равно логин + @ + почтовый домен)
userMailAttribute=mail
# Почтовый домен
userMailDomain=local.domain
# Фильтр пользователей
userFilter=(objectclass=user)
Полученный таким образом скрипт скармливаем Jira в разделе Administration → Options & Settings → Jelly Runner и она, немного подумав, создает пользователей и группы.
Идея прозрачной аутентификации заключается в следующем: аутентифицирует пользователей SPNEGO и передаёт JIRA в переменной REMOTE USER логин уже вошедшего пользователя. Минус заключается в том, что при такой схеме пользователи, которых нет в AD, зайти в JIRA не смогут, однако лучше я ничего не придумал.
Для начала проверяем работоспособность [4] SPNEGO. Для этого скачиваем три файла: krb5.conf [5], login.conf [6] и HelloKDC.java [7].
В файле krb5.conf в секции [libdefaults] пропущена одна строка, не забудьте её вписать. После изменения настроек файл krb5.conf должен выглядеть примерно так:
[libdefaults]
default_realm = LOCAL.DOMAIN
default_tkt_enctypes = aes128-cts rc4-hmac des3-cbc-sha1 des-cbc-md5 des-cbc-crc
default_tgs_enctypes = aes128-cts rc4-hmac des3-cbc-sha1 des-cbc-md5 des-cbc-crc
permitted_enctypes = aes128-cts rc4-hmac des3-cbc-sha1 des-cbc-md5 des-cbc-crc
[realms]
LOCAL.DOMAIN = {
kdc = dc5.local.domain
default_domain = LOCAL.DOMAIN
}
[domain_realm]
.LOCAL.DOMAIN = LOCAL.DOMAIN
HelloKDC.java:
...
// Domain (pre-authentication) account
final String username = "test";
// Password for the pre-auth acct.
final String password = "password";
// Name of our krb5 config file
final String krbfile = "krb5.conf";
// Name of our login config file
final String loginfile = "login.conf";
// Name of our login module
final String module = "spnego-client";
...
Скомпилируйте и запустите файл HelloKDC.java, если всё прошло успешно, он выведет кучу строк и в конце Connection test successful.
Далее скачайте набор утилит Support Tools [8], установите их на доменный компьютер администратора и выполните команды:
setspn.exe -A HTTP/domain1 test
setspn.exe -A HTTP/domain2 test
...
setspn.exe -A HTTP/domainN test
, где
Скачайте дистрибутив SPNEGO [9] и положите его в директорию $JIRA_HOME/lib. Откройте файл $JIRA_HOME/conf/web.xml и добавьте в конец (но до закрытия тега web-app) следующие строки:
<filter>
<filter-name>SpnegoHttpFilter</filter-name>
<filter-class>net.sourceforge.spnego.SpnegoHttpFilter</filter-class>
<init-param>
<param-name>spnego.allow.basic</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>spnego.allow.localhost</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>spnego.allow.unsecure.basic</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>spnego.login.client.module</param-name>
<param-value>spnego-client</param-value>
</init-param>
<init-param>
<param-name>spnego.krb5.conf</param-name>
<param-value>krb5.conf</param-value>
</init-param>
<init-param>
<param-name>spnego.login.conf</param-name>
<param-value>login.conf</param-value>
</init-param>
<init-param>
<param-name>spnego.preauth.username</param-name>
<param-value>test</param-value>
</init-param>
<init-param>
<param-name>spnego.preauth.password</param-name>
<param-value>password</param-value>
</init-param>
<init-param>
<param-name>spnego.login.server.module</param-name>
<param-value>spnego-server</param-value>
</init-param>
<init-param>
<param-name>spnego.prompt.ntlm</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>spnego.logger.level</param-name>
<param-value>1</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>SpnegoHttpFilter</filter-name>
<url-pattern>*</url-pattern>
</filter-mapping>
Не забудьте заменить имя пользователя и пароль на свои.
Пришло время написать аутентификатор, в задачи которого входит получение логина вошедшего пользователя от SPNEGO, добавления к этому логину почтового домена (т.к. логинами в JIRA выступают почтовые адреса) и заполнение свойства, содержащего телефон, у пользователей, у которых этого свойства ещё нет. В поставке SDK для плагинов JIRA 4.x не нашлось место классу JiraOsUserAuthenticator, на основе которого будет написан наш аутентификатор, т.к. этот класс уже является устаревшим. Поэтому его надо вручную добавить в библиотеки при компиляции, взять можно в $JIRA_HOME/atlassian-jira/WEB-INF/classes/com/atlassian/jira/security/login/.
Собственно, код аутентификатора:
import java.security.Principal;
import java.util.Properties;
import java.util.Random;
import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.atlassian.core.user.preferences.Preferences;
import com.atlassian.jira.ComponentManager;
import com.atlassian.jira.security.login.JiraOsUserAuthenticator;
import com.atlassian.jira.user.preferences.UserPreferencesManager;
import com.atlassian.jira.user.util.UserUtil;
import com.opensymphony.user.Group;
import com.opensymphony.user.User;
@SuppressWarnings("deprecation")
public class RemoteAuthenticator extends JiraOsUserAuthenticator {
private static final long serialVersionUID = 1L;
private UserPreferencesManager preferencesManager = ComponentManager
.getInstance().getUserPreferencesManager();
// Адрес сервера LDAP
private final String ADDRESS_NAME = "java.naming.provider.url";
private final String ADDRESS_VALUE = "ldap://127.0.0.1:389";
// DN пользователя для соединения
private final String LOGIN_NAME = "java.naming.security.principal";
private final String LOGIN_VALUE = "cn=test,ou=users,dc=local,dc=domain";
// Пароль пользователя
private final String PASSWORD_NAME = "java.naming.security.credentials";
private final String PASSWORD_VALUE = "password";
// Базовый DN
private final String BASEDN = "ou=users,dc=local,dc=domain";
// Атрибут логина
private final String LOGIN_ATTRIBUTE = "mail";
// Атрибут имени (если не указано, равно логину)
private final String NAME_ATTRIBUTE = "displayName";
// Атрибут почтового адреса (если не указано,
// равен логин + @ + почтовый домен)
private final String MAIL_ATTRIBUTE = "mail";
// Атрибут телефона
private final String PHONE_ATTRIBUTE = "telephoneNumber";
// Почтовый домен
private final String DOMAIN = "local.domain";
@Override
public Principal getUser(HttpServletRequest request,
HttpServletResponse response) {
Principal user = null;
if (request.getSession() != null && request.getSession().
getAttribute(JiraOsUserAuthenticator.LOGGED_IN_KEY) != null) {
// Пользователь уже авторизован
user = (Principal) request.getSession().getAttribute(
JiraOsUserAuthenticator.LOGGED_IN_KEY);
} else {
String remote = request.getRemoteUser();
if (remote != null) {
// Получен логин от SPNEGO
if (!remote.endsWith('@' + DOMAIN)) {
/*
* Добавляем к логину почтовый домен,
* чтобы получить почтовый адрес.
* Данный метод вызывается дважды при входе,
* поэтому добавлять надо только однажды
*/
remote += '@' + DOMAIN;
}
user = getUser(remote.toLowerCase());
if (user == null) {
/*
* Пользователя с таким логином нет в JIRA,
* создаем
*/
try {
user = createUser(remote.toLowerCase());
} catch (Exception exception) {
exception.printStackTrace(System.err);
}
} else {
Preferences preferences = preferencesManager
.getPreferences((User) user);
String phone = preferences
.getString(UserUtil.META_PROPERTY_PREFIX + "mobile");
if (phone == null || phone.isEmpty()) {
/*
* У пользователя не заполнено свойство с телефоном,
* заполняем
*/
try {
phone = getPhone(remote.toLowerCase());
if (phone != null) {
preferences.setString(
UserUtil.META_PROPERTY_PREFIX
+ "mobile", phone);
}
} catch (Exception exception) {
exception.printStackTrace(System.err);
}
}
}
request.getSession().setAttribute(
JiraOsUserAuthenticator.LOGGED_IN_KEY, user);
request.getSession().setAttribute(
JiraOsUserAuthenticator.LOGGED_OUT_KEY, null);
}
}
return user;
}
private User createUser(String login) throws Exception {
DirContext context = null;
try {
Properties properties = new Properties();
properties.put(ADDRESS_NAME, ADDRESS_VALUE);
properties.put(LOGIN_NAME, LOGIN_VALUE);
properties.put(PASSWORD_NAME, PASSWORD_VALUE);
properties.put("java.naming.factory.initial",
"com.sun.jndi.ldap.LdapCtxFactory");
ComponentManager componentManager = ComponentManager.getInstance();
UserUtil userUtil = componentManager.getUserUtil();
context = new InitialDirContext(properties);
SearchControls controls = new SearchControls();
controls.setReturningAttributes(new String[] { "distinguishedName",
NAME_ATTRIBUTE, MAIL_ATTRIBUTE, PHONE_ATTRIBUTE });
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration users = context.search(BASEDN, '('
+ LOGIN_ATTRIBUTE + '=' + login + ')', controls);
if (users != null && users.hasMoreElements()) {
/*
* Пользователь с таким логином (почтовым адресом)
* найден в AD
*/
SearchResult result = (SearchResult) users.next();
Attribute attribute = result.getAttributes()
.get(NAME_ATTRIBUTE);
String userName;
if (attribute != null) {
userName = (String) attribute.get();
} else {
userName = login;
}
attribute = result.getAttributes().get(MAIL_ATTRIBUTE);
String userMail;
if (attribute != null) {
userMail = (String) attribute.get();
} else {
userMail = login.replace(' ', '.') + '@' + DOMAIN;
}
attribute = result.getAttributes().get(PHONE_ATTRIBUTE);
String userPhone = null;
if (attribute != null) {
userPhone = ((String) attribute.get()).replace("(", "")
.replace(")", "");
}
String userDN = (String) result.getAttributes()
.get("distinguishedName").get();
// Генерируем случайный пароль
String userPassword = Integer.toHexString(new Random().nextInt());
// Создаем пользователя
User user = userUtil.createUserNoEvent(login, userPassword,
userMail, userName);
// Если в AD указан телефон, заполняем свойство
if (userPhone != null) {
preferencesManager.getPreferences(user).setString(
UserUtil.META_PROPERTY_PREFIX + "mobile", userPhone);
}
/*
* Добавляем пользователя в группу JIRA, пришелшую из AD,
* если такая есть
*/
int index = userDN.indexOf("OU=") + 3;
String groupName = "[AD] "
+ userDN.substring(index, userDN.indexOf(',', index));
Group group = userUtil.getGroup(groupName);
if (group != null) {
userUtil.addUserToGroup(group, user);
}
return user;
} else {
return null;
}
} finally {
if (context != null) {
context.close();
}
}
}
private String getPhone(String username) throws Exception {
DirContext context = null;
try {
Properties properties = new Properties();
properties.put(ADDRESS_NAME, ADDRESS_VALUE);
properties.put(LOGIN_NAME, LOGIN_VALUE);
properties.put(PASSWORD_NAME, PASSWORD_VALUE);
properties.put("java.naming.factory.initial",
"com.sun.jndi.ldap.LdapCtxFactory");
context = new InitialDirContext(properties);
SearchControls controls = new SearchControls();
controls.setReturningAttributes(new String[] { PHONE_ATTRIBUTE });
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration users = context.search(BASEDN, '('
+ LOGIN_ATTRIBUTE + '=' + username + ')', controls);
if (users != null && users.hasMoreElements()) {
SearchResult result = (SearchResult) users.next();
Attribute attribute = result.getAttributes().get(
PHONE_ATTRIBUTE);
String userPhone = null;
if (attribute != null) {
userPhone = ((String) attribute.get()).replace("(", "")
.replace(")", "");
}
return userPhone;
} else {
return null;
}
} finally {
if (context != null) {
context.close();
}
}
}
}
Компилируем, кладем в $JIRA_HOME/atlassian-jira/WEB-INF/classes, открываем файл $JIRA_HOME/atlassian-jira/WEB-INF/classes/seraph-config.xml и меняем в нем строку
<authenticator class="com.atlassian.jira.security.login.JiraOsUserAuthenticator"/>
на
<authenticator class="RemoteAuthenticator"/>
После этого можно перезапустить JIRA, настроить аутентификацию по Kerberos в своем любимом браузере и проверить работоспособность. Для недоменных компьютеров должно появляться окошко BASIC-авторизации, так что они тоже смогут зайти (если у них есть учетные записи в AD).
Вдобавок была написана служба, которая периодически синхронизирует пользователей в AD и в JIRA и добавляет текстовую метку к именам тех, которых нет в AD. Вот её код:
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import com.atlassian.configurable.ObjectConfiguration;
import com.atlassian.configurable.ObjectConfigurationException;
import com.atlassian.core.user.preferences.Preferences;
import com.atlassian.jira.ComponentManager;
import com.atlassian.jira.event.user.UserEventType;
import com.atlassian.jira.service.AbstractService;
import com.atlassian.jira.user.preferences.UserPreferencesManager;
import com.atlassian.jira.user.util.UserUtil;
import com.opensymphony.module.propertyset.PropertySet;
import com.opensymphony.user.Group;
import com.opensymphony.user.User;
@SuppressWarnings("deprecation")
public class Importer extends AbstractService implements ObjectConfiguration {
// Адрес сервера LDAP
private final String ADDRESS_NAME = "java.naming.provider.url";
private String ADDRESS_VALUE = "ldap://127.0.0.1:389";
// DN пользователя для соединения
private final String LOGIN_NAME = "java.naming.security.principal";
private String LOGIN_VALUE = "cn=test,ou=users,dc=local,dc=domain";
// Пароль пользователя
private final String PASSWORD_NAME = "java.naming.security.credentials";
private String PASSWORD_VALUE = "password";
// Базовый DN
private final String BASE_NAME = "baseDN";
private String BASE_VALUE = "ou=users,dc=local,dc=domain";
// Атрибут логина
private final String LOGIN_ATTR_NAME = "userLoginAttribute";
private String LOGIN_ATTR_VALUE = "mail";
// Атрибут имени (если не указан, равно логину)
private final String NAME_ATTR_NAME = "userNameAttribute";
private String NAME_ATTR_VALUE = "displayName";
// Атрибут почтового адреса (если не указан, равен логин + @ + почтовый
// домен)
private final String MAIL_ATTR_NAME = "userMailAttribute";
private String MAIL_ATTR_VALUE = "mail";
// Атрибут телефона
private final String PHONE_ATTR_NAME = "userPhoneAttribute";
private String PHONE_ATTR_VALUE = "telephoneNumber";
// Почтовый домен
private final String DOMAIN_NAME = "userMailDomain";
private String DOMAIN_VALUE = "local.domain";
// Фильтр пользователей
private final String FILTER_NAME = "userFilter";
private String FILTER_VALUE = "(objectClass=user)";
// Строка, добавляемая к имени пользователя
private final String FIRED_STRING_NAME = "firedString";
private String FIRED_STRING_VALUE = "z_fired";
@Override
public void init(PropertySet properties)
throws ObjectConfigurationException {
super.init(properties);
for (Object keyObject : properties.getKeys()) {
String key = (String) keyObject;
if (key.equals(ADDRESS_NAME)) {
ADDRESS_VALUE = properties.getString(key);
} else if (key.equals(LOGIN_NAME)) {
LOGIN_VALUE = properties.getString(key);
} else if (key.equals(PASSWORD_NAME)) {
PASSWORD_VALUE = properties.getString(key);
} else if (key.equals(BASE_NAME)) {
BASE_VALUE = properties.getString(key);
} else if (key.equals(NAME_ATTR_NAME)) {
NAME_ATTR_VALUE = properties.getString(key);
} else if (key.equals(LOGIN_ATTR_NAME)) {
LOGIN_ATTR_VALUE = properties.getString(key);
} else if (key.equals(MAIL_ATTR_NAME)) {
MAIL_ATTR_VALUE = properties.getString(key);
} else if (key.equals(PHONE_ATTR_NAME)) {
PHONE_ATTR_VALUE = properties.getString(key);
} else if (key.equals(DOMAIN_NAME)) {
DOMAIN_VALUE = properties.getString(key);
} else if (key.equals(FILTER_NAME)) {
FILTER_VALUE = properties.getString(key);
} else {
FIRED_STRING_VALUE = properties.getString(key);
}
}
}
@Override
public ObjectConfiguration getObjectConfiguration()
throws ObjectConfigurationException {
return this;
}
@Override
public void run() {
try {
importUsers();
checkUsers();
} catch (Exception exception) {
exception.printStackTrace();
}
}
// Синхронизирует пользователей в AD и в JIRA
private void importUsers() throws Exception {
DirContext context = null;
try {
Properties properties = new Properties();
properties.put(ADDRESS_NAME, ADDRESS_VALUE);
properties.put(LOGIN_NAME, LOGIN_VALUE);
properties.put(PASSWORD_NAME, PASSWORD_VALUE);
properties.put("java.naming.factory.initial",
"com.sun.jndi.ldap.LdapCtxFactory");
ComponentManager componentManager = ComponentManager.getInstance();
UserPreferencesManager preferencesManager = componentManager
.getUserPreferencesManager();
UserUtil userUtil = componentManager.getUserUtil();
context = new InitialDirContext(properties);
SearchControls controls = new SearchControls();
controls.setReturningAttributes(new String[] { "distinguishedName",
LOGIN_ATTR_VALUE, NAME_ATTR_VALUE, MAIL_ATTR_VALUE,
PHONE_ATTR_VALUE });
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration users = context.search(BASE_VALUE, FILTER_VALUE,
controls);
while (users != null && users.hasMoreElements()) {
SearchResult result = (SearchResult) users.next();
Attribute attribute = result.getAttributes().get(
LOGIN_ATTR_VALUE);
if (userUtil.getUser((String) attribute.get()) != null) {
// Пользователь уже есть в JIRA
continue;
}
String login = ((String) attribute.get()).toLowerCase();
attribute = result.getAttributes().get(NAME_ATTR_VALUE);
String userName;
if (attribute != null) {
userName = (String) attribute.get();
} else {
userName = login;
}
attribute = result.getAttributes().get(MAIL_ATTR_VALUE);
String userMail;
if (attribute != null) {
userMail = (String) attribute.get();
} else {
userMail = login.replace(' ', '.') + '@' + DOMAIN_VALUE;
}
attribute = result.getAttributes().get(PHONE_ATTR_VALUE);
String userPhone = null;
if (attribute != null) {
userPhone = ((String) attribute.get()).replace("(", "")
.replace(")", "");
}
String userDN = (String) result.getAttributes()
.get("distinguishedName").get();
// Генерируем случайный пароль
String userPassword = Integer.toHexString(new Random()
.nextInt());
// Создаем пользователя
User user = userUtil.createUserWithEvent(login, userPassword,
userMail, userName, UserEventType.USER_CREATED);
if (userPhone != null) {
preferencesManager.getPreferences(user).setString(
UserUtil.META_PROPERTY_PREFIX + "mobile", userPhone);
}
/*
* Добавляем пользователя в группу JIRA,
* пришешдшую из AD, если такая есть
*/
int index = userDN.indexOf("OU=") + 3;
String groupName = "[AD] "
+ userDN.substring(index, userDN.indexOf(',', index));
Group group = userUtil.getGroup(groupName);
if (group != null) {
userUtil.addUserToGroup(group, user);
}
}
} finally {
if (context != null) {
context.close();
}
}
}
// Добавляет метки к тем пользователям, которых нет в AD
private void checkUsers() throws Exception {
DirContext context = null;
try {
Properties properties = new Properties();
properties.put(ADDRESS_NAME, ADDRESS_VALUE);
properties.put(LOGIN_NAME, LOGIN_VALUE);
properties.put(PASSWORD_NAME, PASSWORD_VALUE);
properties.put("java.naming.factory.initial",
"com.sun.jndi.ldap.LdapCtxFactory");
ComponentManager componentManager = ComponentManager.getInstance();
UserPreferencesManager preferencesManager = componentManager
.getUserPreferencesManager();
UserUtil userUtil = componentManager.getUserUtil();
context = new InitialDirContext(properties);
SearchControls controls = new SearchControls();
controls.setReturningAttributes(new String[] { PHONE_ATTR_VALUE });
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
for (User user : userUtil.getAllUsers()) {
NamingEnumeration users = context.search(BASE_VALUE, "(&("
+ LOGIN_ATTR_VALUE + '=' + user.getName() + ')'
+ FILTER_VALUE + ')', controls);
if (users == null || !users.hasMore()) {
// Пользователя нет в AD
if (!user.getFullName().startsWith(FIRED_STRING_VALUE)) {
user.setFullName(FIRED_STRING_VALUE + ' '
+ user.getFullName());
}
} else {
Preferences preferences = preferencesManager.
getPreferences(user);
String phone = preferences.getString(
UserUtil.META_PROPERTY_PREFIX + "mobile");
if (phone == null || phone.isEmpty()) {
// У пользователя не заполнено свойство с телефоном
try {
Attribute attribute = ((SearchResult) users.next())
.getAttributes().get(PHONE_ATTR_VALUE);
if (attribute != null) {
phone = ((String) attribute.get()).replace("(",
"").replace(")", "");
}
if (phone != null) {
preferences.setString(
UserUtil.META_PROPERTY_PREFIX
+ "mobile", phone);
}
} catch (Exception exception) {
exception.printStackTrace(System.err);
}
}
}
}
} finally {
if (context != null) {
context.close();
}
}
}
@Override
public boolean allFieldsHidden() {
return false;
}
@Override
public String getName() {
return "LDAP Importer";
}
@Override
public String getDescription() {
return "Раз в день импортирует пользователей из LDAP и меняет имена пользователей, которых нет в LDAP";
}
@SuppressWarnings("rawtypes")
@Override
public String getDescription(Map params) {
return "Раз в день импортирует пользователей из LDAP и изменяет имя пользователям, которых нет в LDAP";
}
@Override
public String[] getEnabledFieldKeys() {
return new String[] { ADDRESS_NAME, LOGIN_NAME, PASSWORD_NAME,
FILTER_NAME, BASE_NAME, LOGIN_ATTR_NAME, NAME_ATTR_NAME,
MAIL_ATTR_NAME, PHONE_ATTR_NAME, DOMAIN_NAME, FIRED_STRING_NAME };
}
@Override
public String getFieldDefault(String key)
throws ObjectConfigurationException {
if (key.equals(ADDRESS_NAME)) {
return ADDRESS_VALUE;
} else if (key.equals(LOGIN_NAME)) {
return LOGIN_VALUE;
} else if (key.equals(PASSWORD_NAME)) {
return PASSWORD_VALUE;
} else if (key.equals(BASE_NAME)) {
return BASE_VALUE;
} else if (key.equals(NAME_ATTR_NAME)) {
return NAME_ATTR_VALUE;
} else if (key.equals(LOGIN_ATTR_NAME)) {
return LOGIN_ATTR_VALUE;
} else if (key.equals(MAIL_ATTR_NAME)) {
return MAIL_ATTR_VALUE;
} else if (key.equals(PHONE_ATTR_NAME)) {
return PHONE_ATTR_VALUE;
} else if (key.equals(DOMAIN_NAME)) {
return DOMAIN_VALUE;
} else if (key.equals(FILTER_NAME)) {
return FILTER_VALUE;
} else {
return FIRED_STRING_VALUE;
}
}
@Override
public String getFieldDescription(String key)
throws ObjectConfigurationException {
if (key.equals(BASE_NAME)) {
return "Поиск ведется начиная с этого DN";
} else if (key.equals(MAIL_ATTR_NAME)) {
return "Если не указан, почта равна "логин + @ + почтовый домен"";
} else if (key.equals(FIRED_STRING_NAME)) {
return "Добавляется к имени пользователя если его нет в LDAP";
} else if (key.equals(NAME_ATTR_NAME)) {
return "Если не указан, имя равно логину";
}
return "";
}
@Override
public String[] getFieldKeys() {
return new String[] { ADDRESS_NAME, LOGIN_NAME, PASSWORD_NAME,
FILTER_NAME, BASE_NAME, LOGIN_ATTR_NAME, NAME_ATTR_NAME,
MAIL_ATTR_NAME, PHONE_ATTR_NAME, DOMAIN_NAME, FIRED_STRING_NAME };
}
@Override
public String getFieldName(String key) throws ObjectConfigurationException {
if (key.equals(ADDRESS_NAME)) {
return "Адрес сервера LDAP";
} else if (key.equals(LOGIN_NAME)) {
return "Логин пользователя LDAP";
} else if (key.equals(PASSWORD_NAME)) {
return "Пароль пользователя LDAP";
} else if (key.equals(BASE_NAME)) {
return "Базовый DN";
} else if (key.equals(NAME_ATTR_NAME)) {
return "Атрибут имени пользователя";
} else if (key.equals(LOGIN_ATTR_NAME)) {
return "Атрибут логина пользователя";
} else if (key.equals(MAIL_ATTR_NAME)) {
return "Атрибут адреса почты";
} else if (key.equals(PHONE_ATTR_NAME)) {
return "Атрибут телефона";
} else if (key.equals(DOMAIN_NAME)) {
return "Почтовый домен";
} else if (key.equals(FILTER_NAME)) {
return "Фильтр пользователей";
} else {
return "Метка";
}
}
@Override
public int getFieldType(String key) throws ObjectConfigurationException {
return 0;
}
@SuppressWarnings("rawtypes")
@Override
public Map getFieldValues(String key) throws ObjectConfigurationException {
return null;
}
@SuppressWarnings("rawtypes")
@Override
public void init(Map params) {
}
@Override
public boolean isEnabled(String key) {
return true;
}
@Override
public boolean isI18NValues(String key) {
return false;
}
}
Скомпилированный jar-файл (с помощью Jira Plugin SDK) со службой положить в $JIRA_HOME/atlassian-jira/WEB-INF/lib/. Добавить службу можно в разделе Administration → System → Services
На этом всё, надеюсь, информация будет кому-нибудь полезной.
Автор: arinoki
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/windows/4592
Ссылки в тексте:
[1] XoJIoD: http://habrahabr.ru/users/XoJIoD/
[2] утилита: http://confluence.atlassian.com/display/JIRA/Importing+user+from+LDAP
[3] документацией по Jelly-тегам: http://confluence.atlassian.com/display/JIRA041/Jelly+Tags
[4] проверяем работоспособность: http://spnego.sourceforge.net/pre_flight.html
[5] krb5.conf: http://spnego.sourceforge.net/krb5.conf
[6] login.conf: http://spnego.sourceforge.net/login.conf
[7] HelloKDC.java: http://spnego.sourceforge.net/HelloKDC.java
[8] Support Tools: http://en.wikipedia.org/wiki/Windows_Support_Tools
[9] дистрибутив SPNEGO: http://sourceforge.net/projects/spnego/files/
Нажмите здесь для печати.