О Rundeck на Хабре уже писали — вкратце, это Open Source проект, позволяющий упростить и автоматизировать выполнение скриптов на удаленных серверах. В статье также была вскользь упомянута возможность интегрировать Rundeck с существующим LDAP/AD сервером. Для этого используется
Но что если корпоративная политика AD запрещает использование сервиса для целей отличных от IDM (Identity Management) или внесение измений в AD — процесс длящийся несколько недель/месяцев, а интеграция нужна уже сейчас?
Итак, приступим. Сразу же оговорюсь, что код описанный ниже относится к версии 1.5 (Github), хотя сам подход использовался начиная с версии 1.3.0.
«Из коробки» администратору предоставляется возможность использовать несколько вариантов настройки аутентификации и авторизации — выше рассмотренный
Таким образом, всё, что нам необходимо сделать — это «скрестить»
За получение списка ролей из LDAP отвечают методы
Унаследуемся от класса
Теперь, используя конфигурационный файл вида
аутентификация будет проходить через LDAP, а авторизация через csv-подобный .properties-файл.
Последний штрих — часто в LDAP хранятся несколько «версий» одной и той же учетной записи которые могут отличаться лишь в одном или нескольких аттрибутах (причем необязательно именно том, который будет использоваться для аутентификации) — при этом только одна учетная запись будет активной. Чтобы обойти эту проблему (несколько искусственную в общем, но актуальную для нас в частности), слегка модифицируем исходный код оригинального
и добавим необходимое значение для
И напоследок — как уже было сказано выше, впервые нам пришлось модифицировать Rundeck в версии 1.3.0. С тех пор большинство наших патчей ( #29, #42, #36) было принято в основную ветку в том или ином виде, за исключением этого изменения (#474), которое пришлось портировать сначала в версию 1.4.2, а затем и в 1.5. Чтобы избежать этого в дальнейшем, было решено вынести этот код в отдельную библиотеку, которую можно подключать к последующим релизам, не заботясь о совместимости — результаты можно найти на Github'е.
P.S. Если кто-то сможет подсказать, возможна ли конфигурация, упомянутая в #474, буду весьма благодарен — насколько я понял спецификацию
JettyCachingLdapLoginModule
(документация), который позволяет аутентифицировать пользователя по его учетной записи в AD, а также «вытянуть» оттуда ассоциированные с этим пользователем роли.Но что если корпоративная политика AD запрещает использование сервиса для целей отличных от IDM (Identity Management) или внесение измений в AD — процесс длящийся несколько недель/месяцев, а интеграция нужна уже сейчас?
Итак, приступим. Сразу же оговорюсь, что код описанный ниже относится к версии 1.5 (Github), хотя сам подход использовался начиная с версии 1.3.0.
«Из коробки» администратору предоставляется возможность использовать несколько вариантов настройки аутентификации и авторизации — выше рассмотренный
JettyCachingLdapLoginModule
и PropertyFileLoginModule
, который подразумевает хранение на жестком диске файла, содержащего логины, пароли (или их хэши) вместе с ассоциированными с этими логинами ролями.Таким образом, всё, что нам необходимо сделать — это «скрестить»
JettyCachingLdapLoginModule
с PropertyFileLoginModule
чтобы получить JettyCachingHybridLoginModule
, который будет проводить аутентификацию через LDAP
, а авторизацию через csv-подобный файл. При этом постораемся использовать как можно больше существующего кода, надеясь на его качество (по крайней мере на этом этапе).За получение списка ролей из LDAP отвечают методы
getUserRoles
и getUserRolesByDn
(https://github.com/dtolabs/rundeck/blob/release-1.5/rundeck-launcher/rundeck-jetty-server/src/main/java/com/dtolabs/rundeck/jetty/jaas/JettyCachingLdapLoginModule.java?source=cc). Унаследуемся от класса
JettyCachingLdapLoginModule
предварительной изменив модификаторы доступа обеих функций на protected
, добавим дополнительное поле типа PropertyFileLogin
, инициализируемое в переопределенном методе initialize
и заменим реализацию методов getUserRoles
и getUserRolesByDn
соответствующими вызовами к PropertyFileLoginModule
(https://github.com/coiouhkc/rundeck-hybrid-login/blob/master/src/org/abratuhi/rundeck/jetty/jaas/JettyCachingHybridLoginModule.java):...
public class JettyCachingHybridLoginModule extends JettyCachingLdapLoginModule {
private PropertyFileLoginModule propfileModule;
public void initialize(Subject subject, CallbackHandler callbackHandler,
Map sharedState, Map options)
{
super.initialize(subject, callbackHandler, sharedState, options);
propfileModule = new PropertyFileLoginModule();
propfileModule.initialize(subject, callbackHandler, sharedState, options);
}
public UserInfo getUserInfo(String username) throws Exception {
String pwdCredential = getUserCredentials(username);
if (pwdCredential == null) {
return null;
}
pwdCredential = convertCredentialLdapToJetty(pwdCredential);
Credential credential = Credential.getCredential(pwdCredential);
org.mortbay.jetty.jaas.UserInfo localUserInfo = propfileModule.getUserInfo(username);
return new UserInfo(username, credential, localUserInfo.getRoleNames());
}
protected List getUserRoles(DirContext dirContext, String username) throws LoginException,
NamingException {
List result = getUserRolesByDn(dirContext, null, username);
return result;
}
protected List getUserRolesByDn(DirContext dirContext, String userDn, String username) throws LoginException,
NamingException {
List result;
try{
org.mortbay.jetty.jaas.UserInfo localUserInfo = propfileModule.getUserInfo(username);
result = localUserInfo.getRoleNames();
}
catch(Exception e){
result = new ArrayList();
}
return result;
}
}
Теперь, используя конфигурационный файл вида
RDpropertyfilelogin {
org.mortbay.jetty.plus.jaas.spi.JettyCachingHybridLoginModule required
debug="true"
file="/etc/rundeck/realm.properties";
contextFactory="com.sun.jndi.ldap.LdapCtxFactory"
providerUrl="ldap://server:389"
bindDn="cn=Manager,dc=example,dc=com"
bindPassword="secrent"
authenticationMethod="simple"
forceBindingLogin="false"
userBaseDn="ou=People,dc=test1,dc=example,dc=com"
userRdnAttribute="uid"
userIdAttribute="uid"
userPasswordAttribute="userPassword"
userObjectClass="account"
roleBaseDn="ou=Groups,dc=test1,dc=example,dc=com"
roleNameAttribute="cn"
roleUsernameMemberAttribute="memberUid"
roleMemberAttribute="memberUid"
roleObjectClass="posixGroup"
cacheDurationMillis="300000"
reportStatistics="true";
};
аутентификация будет проходить через LDAP, а авторизация через csv-подобный .properties-файл.
Последний штрих — часто в LDAP хранятся несколько «версий» одной и той же учетной записи которые могут отличаться лишь в одном или нескольких аттрибутах (причем необязательно именно том, который будет использоваться для аутентификации) — при этом только одна учетная запись будет активной. Чтобы обойти эту проблему (несколько искусственную в общем, но актуальную для нас в частности), слегка модифицируем исходный код оригинального
JettyCachingLdapLoginModule
:public class JettyCachingLdapLoginModule extends AbstractLoginModule {
...
private String _userAdditionalAndQuery = "";
...
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
...
_userAdditionalAndQuery = getOption(options, "userAdditionalAndQuery", _userAdditionalAndQuery);
...
}
private SearchResult findUser(String username) throws NamingException, LoginException {
...
String filter = "(&(objectClass={0})({1}={2})" + _userAdditionalAndQuery + ")";
...
}
}
и добавим необходимое значение для
userAdditionalAndQuery
в конфигурационный файл jaas-loginmodule.conf.И напоследок — как уже было сказано выше, впервые нам пришлось модифицировать Rundeck в версии 1.3.0. С тех пор большинство наших патчей ( #29, #42, #36) было принято в основную ветку в том или ином виде, за исключением этого изменения (#474), которое пришлось портировать сначала в версию 1.4.2, а затем и в 1.5. Чтобы избежать этого в дальнейшем, было решено вынести этот код в отдельную библиотеку, которую можно подключать к последующим релизам, не заботясь о совместимости — результаты можно найти на Github'е.
P.S. Если кто-то сможет подсказать, возможна ли конфигурация, упомянутая в #474, буду весьма благодарен — насколько я понял спецификацию
JAAS
упомянутую в дискуссии отдельные LoginModule
'и не переходят к следующему только в случае неудачной аутентификации, но не неудачной/пустой авторизации).