barbitoff programmer`s blog

Здесь я публикую заметки из программерской жизни: грабли, на которые мне случилось наступить, проблемы, для которых было найдено элегантное (или не очень) решение, а также все, с чем мне пришлось столкнуться и чем хотелось бы поделиться =)
PS Если хотите меня поблагодарить - на странице есть 3 места, чтобы это сделать =)

среда, 17 августа 2016 г.

Spring Security и аутентификация по Active Directory: ошибка "Failed to locate directory entry for authenticated user"

Проблема

Настроил в приложении, использующем Spring Security, аутентификацию по Active Directory (провайдер ActiveDirectoryLdapAuthenticationProvider), но при попытке входа ловлю ошибку:
2016-08-17 11:20:45.525 ERROR 22440 --- [http-nio-8080-exec-154] ctiveDirectoryLdapAuthenticationProvider : Failed to locate directory entry for authenticated user: user@domain.local
javax.naming.NameNotFoundException: [LDAP: error code 32 - 0000208D: NameErr: DSID-0310020A, problem 2001 (NO_OBJECT), data 0, best match of:
'DC=domain,DC=local'

 ]
at com.sun.jndi.ldap.LdapCtx.mapErrorCode(LdapCtx.java:3160) ~[na:1.8.0_60]
at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:3081) ~[na:1.8.0_60]
at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2888) ~[na:1.8.0_60]
at com.sun.jndi.ldap.LdapCtx.searchAux(LdapCtx.java:1846) ~[na:1.8.0_60]
at com.sun.jndi.ldap.LdapCtx.c_search(LdapCtx.java:1769) ~[na:1.8.0_60]
at com.sun.jndi.ldap.LdapCtx.c_search(LdapCtx.java:1786) ~[na:1.8.0_60]
at com.sun.jndi.toolkit.ctx.ComponentDirContext.p_search(ComponentDirContext.java:418) ~[na:1.8.0_60]
at com.sun.jndi.toolkit.ctx.PartialCompositeDirContext.search(PartialCompositeDirContext.java:396) ~[na:1.8.0_60]
at javax.naming.directory.InitialDirContext.search(InitialDirContext.java:297) ~[na:1.8.0_60]
at org.springframework.security.ldap.SpringSecurityLdapTemplate.searchForSingleEntryInternal(SpringSecurityLdapTemplate.java:333) ~[spring-security-ldap-4.0.3.RELEASE.jar:4.0.3.RELEASE]
at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.searchForUser(ActiveDirectoryLdapAuthenticationProvider.java:310) ~[spring-security-ldap-4.0.3.RELEASE.jar:4.0.3.RELEASE]
at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.doAuthentication(ActiveDirectoryLdapAuthenticationProvider.java:144) ~[spring-security-ldap-4.0.3.RELEASE.jar:4.0.3.RELEASE]
at org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider.authenticate(AbstractLdapAuthenticationProvider.java:82) [spring-security-ldap-4.0.3.RELEASE.jar:4.0.3.RELEASE]
Причина

Провайдер ActiveDirectoryLdapAuthenticationProvider работает следующим образом: 
  1. сначала он пытается забиндиться к AD с указанным логином / паролем
  2. после этого ищет объект пользователя по совпадению введенного логина и атрибута userPrincipalName
Попробовал выполнить аналогичный поиск по введенному логину в AD через curl (http://barbitoff.blogspot.ru/2016/08/ldap-ad-ldapsearch.html), поиск не дал результатов. Посмотрел на объект пользователя - он вообще не имеет атрибута userPrincipalName (не большой специалист по AD, поэтому не могу сказать, почему). К счастью, фильтр, используемый для поиска пользователя в ActiveDirectoryLdapAuthenticationProvider кастомизируется путем вызова setSearchFilter().

Как подключиться по LDAP к AD, если под рукой нет ldapsearch

Проблема

Есть AD-сервер, по которому есть следующая информация:
  • IP
  • юзер и пароль
Хочется полазить по нему, но под рукой есть только Linux машина без ldapsearch.

Решение

Зато на машине нашелся curl. Выполняем:
curl -u <user>@<domain>:<password> "ldap://<AD_IP>:389/"
и получаем в выдаче уже кое-какую интересную информацию о сервере. Далее можно выполнять поисковые запросы (см. https://technet.microsoft.com/en-us/library/aa996205(v=exchg.65).aspx) и искать все, что нужно. Например, найти пользователя по логину:
curl -u <user>@<domain>:<password> "ldap://<AD_IP>:389/DC=company,DC=org?memberOf,sAMAccountName?sub?(sAMAccountName=someusr)"
Или посмотреть список всех OU первого уровня:
curl -u <user>@<domain>:<password> "ldap://<AD_IP>:389/DC=company,DC=org?distinguishedName?one?(objectClass=organizationalUnit)" 
По формату используемого LDAP URL можно посмотреть в соотв. RFC: http://www.ietf.org/rfc/rfc2255.txt.

четверг, 11 августа 2016 г.

Spring Security и "javax.naming.PartialResultException [Root exception is javax.naming.CommunicationException: xxx.yyy:389 [Root exception is java.net.UnknownHostException : xxx.yyy]]"

Проблема

Неожиданно в приложении, использующем для аутентификации и авторизации Spring Security + Misrosoft Active Directory, перестала работать аутентификация из-за ошибки:
javax.naming.PartialResultException [Root exception is javax.naming.CommunicationException: xxx.yyy:389 [Root exception is java.net.UnknownHostException: xxx.yyy]]
При этом хостнэйм xxx.yyy не фигурирует в настройках Spring Security (там указан ip сервера AD), единственная корреляция этого хостнэйма с настройками заключается в том, что DC=xxx,DC=yyy - это корневой DN нашего AD.

Решение

Погуглив, нашел, что ошибка PartialResultException говорит о том, что AD-сервер вернул ссылку на вышестоящий AD-лес, и с обработкой этой ссылки возникла какая-то проблема (в нашем случае, ссылка содержит неизвестный хостнэйм xxx.yyy).  Одно из возможных решений - изменить порт, по которому приложение соединяется с AD, с 389 на 3268, по этому порту AD не возвращает ссылок. Спасибо http://stackoverflow.com/questions/16412236/how-to-resolve-javax-naming-partialresultexception.

среда, 3 августа 2016 г.

SLES: пропало место на диске

Проблема

Случилась тут занятная ситуация. На сервере под SLES 11 кончилось место, df показывает, что занято 100% на корневой ФС (всего объем раздела, смонтированного как корневой - более 60Гб). Стали искать, куда же место утекло, смотрим:
du -sh /*
Видим пару папочек по 2-3Гб, остальное - копейки. Суммарно 60Гб совсем не получается, т.е. место вроде бы занято, а чем именно - не понятно.

Решение

Оказалось, в Linux есть интересная особенность, которой нет в Win. Файл может быть удален (т.е. не иметь ни одной ссылки из файловой системы), но при этом продолжать использоваться программами, которые сохранили handle от него, и, соответственно, занимать место на диске. Посмотреть такие файлы можно командой:
lsof +L1
Она как раз покажет файлы, имеющие менее 1 ссылки (т.е. 0 ссылок) в файловой системе. 
В нашем случае это оказались увесистые файлы логов одной из служб, кто-то удалил эти логи, не останавливая службу, и она продолжила их использовать. Решение - останавливаем службу, место на диске магическим образом само расчищается, и секунд через 30 уже имеем 50 свободных гигов.

вторник, 2 августа 2016 г.

SLES: добавление постоянного маршрута

Есть несколько способов, один из них:
  1. Создаем исполняемый файл в /etc/sysconfig/network/if-up.d/. Он будет выполняться при каждом поднятии интерфейса
  2. Прописываем в него команду добавления нужного маршрута, например:
    route add -host 10.1.1.2 gw 192.168.1.100

четверг, 30 июня 2016 г.

SLES 11 и внезапно пропадающий процесс WSO2 ESB

Есть SLES 11, на нем крутится WSO2 ESB 4.9.0. Периодически процесс WSO2 ESB просто пропадает, в логах самой шины никаких ошибок, как будто его просто кто-то кильнул. Возникло подозрение, что процесс киляет сама ОС из-за нехватки ОП. Идем в /var/log/warn, видим там:
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.048428] java invoked oom-killer: gfp_mask=0x201da, order=0, oom_adj=0, oom_score_adj=0
...
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.048618] 88800 total pagecache pages
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.048619] 53685 pages in swap cache
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.048620] Swap cache stats: add 8392477, delete 8338792, find 21248605/22043373
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.048622] Free swap  = 0kB
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.048631] Total swap = 1048572kB
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.057372] 1048560 pages RAM
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.057374] 35908 pages reserved
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.057375] 80915 pages shared
Jun 30 10:21:12 ESBint2-60141 kernel: [3866020.057376] 950754 pages non-shared

Jun 30 10:21:13 ESBint2-60141 kernel: [3866020.057588] Out of memory: Kill process 26797 (java) score 432 or sacrifice child
Jun 30 10:21:13 ESBint2-60141 kernel: [3866020.057707] Killed process 26797 (java) total-vm:3475068kB, anon-rss:2180320kB, file-rss:0kB
Сравниваем указанный в последней строчке pid со значением в файле wso2carbon.pid в директории установки WSO2 ESB - совпадает. Диагноз - шина убивается системой из-за нехватки физической памяти.

вторник, 21 июня 2016 г.

requirejs и управление браузерным кэшированием с помощью конфигурационного параметра urlArgs

Задача

В проекте, использующем requirejs, встала острая необходимость управлять кэшированием js-файлов браузером. Ситуация типичная: в приложении обновляется некоторый js-файл, но клиент продолжает пользоваться старой версией файла, т.к. браузер его закэшировал (в т.н. называемый Back/Forward Cache, или BFCache: http://www.pvsm.ru/javascript/61476). И пока кэш не просрочится, ситуация не изменится. Вот как данный случай выглядит в Firebug:


Причем данная ситуация может возникать как в продуктивном развертывании, так и при разработке / отладке, и ее нужно как-то брать под свой контроль, т.к. заставлять пользователя чистить кэш - не лучшее решение.

Решение

Конечно, управлять кэшированием можно на стороне сервера, с помощью заголовков Expires / Last-Modified / Cache-Control / Pragma. Но, во-первых, предлагаемый здесь способ более гибкий. А, во-вторых, если управлять кэшированием вы решили не на самом старте проекта, а лишь в какой-то момент его жизни после выхода в продакшен, вы никакими серверными махинациями не заставите браузер раскэшировать то, что он уже закэшировал ранее.
Итак, про requeirejs. Конфигурация requirejs имеет параметр urlArgs, позволяющий добавлять GET-аргументы к URL-ам, по которым загружаются js-файлы. Например, при задании этого параметры равным "v=1" файл formatter.js будет грузиться по URL'у formatter.js?v=1. В новых версиях requirejs есть возможность использовать функцию вместо статичной строки, но сейчас не об этом. Главное, что для браузера formatter.js и formatter.js?v=1 - различные  URL, и даже закешировав ранее файл formatter.js браузер все равно пойдет на сервер за formatter.js?v=1 (хотя, сервер, конечно, по обоим URL'ам выдаст один и тот же файл, т.к. это статичный ресурс и GET-параметры для него игнорируются).
При поиске решения я наткнулся на интересную статью: http://blog.johnnyreilly.com/2014/03/caching-and-cache-busting-with-requirejs.html. В ней приводится рекомендация по использованию параметра urlArgs для управления кэшированием, которую я успешно и применил. Автор предлагает 2 разных подхода для разработки и продуктивного развертывания проекта:
  • В процессе разработки удобно, когда вообще ничего не кэшируется, и любые правки на сервере сразу же попадают в браузер. В данном случае задаем параметр urlArgs равным "v=" +  (new Date()).getTime(). В итоге имеем при каждой загрузке страницы новые URL'ы, и браузер все js-файлы гарантированно загружает с сервера
  • В продуктивном развертывании необходим баланс между актуальностью файлов и потреблением сетевых ресурсов, так что вариант, описанный выше, не подходит: файлы на сервере меняются не часто, и на какой-то промежуток времени их все же хорошо было бы кэшировать. Здесь можно воспользоваться следующим подходом: устанавливаем параметр urlArgs  равным "v=<productVersion>", где <productVersion> - версия Вашего продукта (сайта). Т.о., при выпуске и установке в продуктив новой версии Вашего продукта все клиенты при первом обращении к сайту получат актуальные версии js-файлов, т.к. изменилась версия продукта, а, значит, и все URL-ы, по которым загружаются js-ки. Далее же, до выхода следующей версии, браузер может (и будет) спокойно кэшировать js-файлы, т.к. они гарантированно остаются неизменными.