Как стать автором
Обновить

Rundeck и его интеграция с корпоративным LDAP сервером

О Rundeck на Хабре уже писали — вкратце, это Open Source проект, позволяющий упростить и автоматизировать выполнение скриптов на удаленных серверах. В статье также была вскользь упомянута возможность интегрировать Rundeck с существующим LDAP/AD сервером. Для этого используется 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'и не переходят к следующему только в случае неудачной аутентификации, но не неудачной/пустой авторизации).
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.