You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LdapUserService.java 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. /*
  2. * Copyright 2012 John Crygier
  3. * Copyright 2012 gitblit.com
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. package com.gitblit;
  18. import java.io.File;
  19. import java.net.URI;
  20. import java.net.URISyntaxException;
  21. import java.security.GeneralSecurityException;
  22. import java.util.Arrays;
  23. import java.util.HashMap;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.concurrent.TimeUnit;
  27. import java.util.concurrent.atomic.AtomicLong;
  28. import org.slf4j.Logger;
  29. import org.slf4j.LoggerFactory;
  30. import com.gitblit.Constants.AccountType;
  31. import com.gitblit.manager.IRuntimeManager;
  32. import com.gitblit.models.TeamModel;
  33. import com.gitblit.models.UserModel;
  34. import com.gitblit.utils.ArrayUtils;
  35. import com.gitblit.utils.StringUtils;
  36. import com.unboundid.ldap.sdk.Attribute;
  37. import com.unboundid.ldap.sdk.DereferencePolicy;
  38. import com.unboundid.ldap.sdk.ExtendedResult;
  39. import com.unboundid.ldap.sdk.LDAPConnection;
  40. import com.unboundid.ldap.sdk.LDAPException;
  41. import com.unboundid.ldap.sdk.LDAPSearchException;
  42. import com.unboundid.ldap.sdk.ResultCode;
  43. import com.unboundid.ldap.sdk.SearchRequest;
  44. import com.unboundid.ldap.sdk.SearchResult;
  45. import com.unboundid.ldap.sdk.SearchResultEntry;
  46. import com.unboundid.ldap.sdk.SearchScope;
  47. import com.unboundid.ldap.sdk.SimpleBindRequest;
  48. import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
  49. import com.unboundid.util.ssl.SSLUtil;
  50. import com.unboundid.util.ssl.TrustAllTrustManager;
  51. /**
  52. * Implementation of an LDAP user service.
  53. *
  54. * @author John Crygier
  55. */
  56. public class LdapUserService extends GitblitUserService {
  57. public static final Logger logger = LoggerFactory.getLogger(LdapUserService.class);
  58. private IStoredSettings settings;
  59. private AtomicLong lastLdapUserSync = new AtomicLong(0L);
  60. public LdapUserService() {
  61. super();
  62. }
  63. private long getSynchronizationPeriod() {
  64. final String cacheDuration = settings.getString(Keys.realm.ldap.ldapCachePeriod, "2 MINUTES");
  65. try {
  66. final String[] s = cacheDuration.split(" ", 2);
  67. long duration = Long.parseLong(s[0]);
  68. TimeUnit timeUnit = TimeUnit.valueOf(s[1]);
  69. return timeUnit.toMillis(duration);
  70. } catch (RuntimeException ex) {
  71. throw new IllegalArgumentException(Keys.realm.ldap.ldapCachePeriod + " must have format '<long> <TimeUnit>' where <TimeUnit> is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'");
  72. }
  73. }
  74. @Override
  75. public void setup(IStoredSettings settings) {
  76. this.settings = settings;
  77. String file = settings.getString(Keys.realm.ldap.backingUserService, "${baseFolder}/users.conf");
  78. IRuntimeManager runtimeManager = GitBlit.getManager(IRuntimeManager.class);
  79. File realmFile = runtimeManager.getFileOrFolder(file);
  80. serviceImpl = createUserService(realmFile);
  81. logger.info("LDAP User Service backed by " + serviceImpl.toString());
  82. synchronizeLdapUsers();
  83. }
  84. protected synchronized void synchronizeLdapUsers() {
  85. final boolean enabled = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.enable, false);
  86. if (enabled) {
  87. if (System.currentTimeMillis() > (lastLdapUserSync.get() + getSynchronizationPeriod())) {
  88. logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server));
  89. final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.removeDeleted, true);
  90. LDAPConnection ldapConnection = getLdapConnection();
  91. if (ldapConnection != null) {
  92. try {
  93. String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
  94. String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid");
  95. String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
  96. accountPattern = StringUtils.replace(accountPattern, "${username}", "*");
  97. SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
  98. if (result != null && result.getEntryCount() > 0) {
  99. final Map<String, UserModel> ldapUsers = new HashMap<String, UserModel>();
  100. for (SearchResultEntry loggingInUser : result.getSearchEntries()) {
  101. final String username = loggingInUser.getAttribute(uidAttribute).getValue();
  102. logger.debug("LDAP synchronizing: " + username);
  103. UserModel user = getUserModel(username);
  104. if (user == null) {
  105. user = new UserModel(username);
  106. }
  107. if (!supportsTeamMembershipChanges())
  108. getTeamsFromLdap(ldapConnection, username, loggingInUser, user);
  109. // Get User Attributes
  110. setUserAttributes(user, loggingInUser);
  111. // store in map
  112. ldapUsers.put(username.toLowerCase(), user);
  113. }
  114. if (deleteRemovedLdapUsers) {
  115. logger.debug("detecting removed LDAP users...");
  116. for (UserModel userModel : super.getAllUsers()) {
  117. if (Constants.EXTERNAL_ACCOUNT.equals(userModel.password)) {
  118. if (! ldapUsers.containsKey(userModel.username)) {
  119. logger.info("deleting removed LDAP user " + userModel.username + " from backing user service");
  120. super.deleteUser(userModel.username);
  121. }
  122. }
  123. }
  124. }
  125. super.updateUserModels(ldapUsers.values());
  126. if (!supportsTeamMembershipChanges()) {
  127. final Map<String, TeamModel> userTeams = new HashMap<String, TeamModel>();
  128. for (UserModel user : ldapUsers.values()) {
  129. for (TeamModel userTeam : user.teams) {
  130. userTeams.put(userTeam.name, userTeam);
  131. }
  132. }
  133. updateTeamModels(userTeams.values());
  134. }
  135. }
  136. lastLdapUserSync.set(System.currentTimeMillis());
  137. } finally {
  138. ldapConnection.close();
  139. }
  140. }
  141. }
  142. }
  143. }
  144. private LDAPConnection getLdapConnection() {
  145. try {
  146. URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));
  147. String ldapHost = ldapUrl.getHost();
  148. int ldapPort = ldapUrl.getPort();
  149. String bindUserName = settings.getString(Keys.realm.ldap.username, "");
  150. String bindPassword = settings.getString(Keys.realm.ldap.password, "");
  151. LDAPConnection conn;
  152. if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) { // SSL
  153. SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
  154. conn = new LDAPConnection(sslUtil.createSSLSocketFactory());
  155. } else if (ldapUrl.getScheme().equalsIgnoreCase("ldap") || ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) { // no encryption or StartTLS
  156. conn = new LDAPConnection();
  157. } else {
  158. logger.error("Unsupported LDAP URL scheme: " + ldapUrl.getScheme());
  159. return null;
  160. }
  161. conn.connect(ldapHost, ldapPort);
  162. if (ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
  163. SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
  164. ExtendedResult extendedResult = conn.processExtendedOperation(
  165. new StartTLSExtendedRequest(sslUtil.createSSLContext()));
  166. if (extendedResult.getResultCode() != ResultCode.SUCCESS) {
  167. throw new LDAPException(extendedResult.getResultCode());
  168. }
  169. }
  170. if ( ! StringUtils.isEmpty(bindUserName) || ! StringUtils.isEmpty(bindPassword)) {
  171. conn.bind(new SimpleBindRequest(bindUserName, bindPassword));
  172. }
  173. return conn;
  174. } catch (URISyntaxException e) {
  175. logger.error("Bad LDAP URL, should be in the form: ldap(s|+tls)://<server>:<port>", e);
  176. } catch (GeneralSecurityException e) {
  177. logger.error("Unable to create SSL Connection", e);
  178. } catch (LDAPException e) {
  179. logger.error("Error Connecting to LDAP", e);
  180. }
  181. return null;
  182. }
  183. /**
  184. * Credentials are defined in the LDAP server and can not be manipulated
  185. * from Gitblit.
  186. *
  187. * @return false
  188. * @since 1.0.0
  189. */
  190. @Override
  191. public boolean supportsCredentialChanges() {
  192. return false;
  193. }
  194. /**
  195. * If no displayName pattern is defined then Gitblit can manage the display name.
  196. *
  197. * @return true if Gitblit can manage the user display name
  198. * @since 1.0.0
  199. */
  200. @Override
  201. public boolean supportsDisplayNameChanges() {
  202. return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.displayName, ""));
  203. }
  204. /**
  205. * If no email pattern is defined then Gitblit can manage the email address.
  206. *
  207. * @return true if Gitblit can manage the user email address
  208. * @since 1.0.0
  209. */
  210. @Override
  211. public boolean supportsEmailAddressChanges() {
  212. return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.email, ""));
  213. }
  214. /**
  215. * If the LDAP server will maintain team memberships then LdapUserService
  216. * will not allow team membership changes. In this scenario all team
  217. * changes must be made on the LDAP server by the LDAP administrator.
  218. *
  219. * @return true or false
  220. * @since 1.0.0
  221. */
  222. @Override
  223. public boolean supportsTeamMembershipChanges() {
  224. return !settings.getBoolean(Keys.realm.ldap.maintainTeams, false);
  225. }
  226. @Override
  227. protected AccountType getAccountType() {
  228. return AccountType.LDAP;
  229. }
  230. @Override
  231. public UserModel authenticate(String username, char[] password) {
  232. if (isLocalAccount(username)) {
  233. // local account, bypass LDAP authentication
  234. return super.authenticate(username, password);
  235. }
  236. String simpleUsername = getSimpleUsername(username);
  237. LDAPConnection ldapConnection = getLdapConnection();
  238. if (ldapConnection != null) {
  239. try {
  240. // Find the logging in user's DN
  241. String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
  242. String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
  243. accountPattern = StringUtils.replace(accountPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
  244. SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
  245. if (result != null && result.getEntryCount() == 1) {
  246. SearchResultEntry loggingInUser = result.getSearchEntries().get(0);
  247. String loggingInUserDN = loggingInUser.getDN();
  248. if (isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) {
  249. logger.debug("LDAP authenticated: " + username);
  250. UserModel user = null;
  251. synchronized (this) {
  252. user = getUserModel(simpleUsername);
  253. if (user == null) // create user object for new authenticated user
  254. user = new UserModel(simpleUsername);
  255. // create a user cookie
  256. if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {
  257. user.cookie = StringUtils.getSHA1(user.username + new String(password));
  258. }
  259. if (!supportsTeamMembershipChanges())
  260. getTeamsFromLdap(ldapConnection, simpleUsername, loggingInUser, user);
  261. // Get User Attributes
  262. setUserAttributes(user, loggingInUser);
  263. // Push the ldap looked up values to backing file
  264. super.updateUserModel(user);
  265. if (!supportsTeamMembershipChanges()) {
  266. for (TeamModel userTeam : user.teams)
  267. updateTeamModel(userTeam);
  268. }
  269. }
  270. return user;
  271. }
  272. }
  273. } finally {
  274. ldapConnection.close();
  275. }
  276. }
  277. return null;
  278. }
  279. /**
  280. * Set the admin attribute from team memberships retrieved from LDAP.
  281. * If we are not storing teams in LDAP and/or we have not defined any
  282. * administrator teams, then do not change the admin flag.
  283. *
  284. * @param user
  285. */
  286. private void setAdminAttribute(UserModel user) {
  287. if (!supportsTeamMembershipChanges()) {
  288. List<String> admins = settings.getStrings(Keys.realm.ldap.admins);
  289. // if we have defined administrative teams, then set admin flag
  290. // otherwise leave admin flag unchanged
  291. if (!ArrayUtils.isEmpty(admins)) {
  292. user.canAdmin = false;
  293. for (String admin : admins) {
  294. if (admin.startsWith("@")) { // Team
  295. if (user.getTeam(admin.substring(1)) != null)
  296. user.canAdmin = true;
  297. } else
  298. if (user.getName().equalsIgnoreCase(admin))
  299. user.canAdmin = true;
  300. }
  301. }
  302. }
  303. }
  304. private void setUserAttributes(UserModel user, SearchResultEntry userEntry) {
  305. // Is this user an admin?
  306. setAdminAttribute(user);
  307. // Don't want visibility into the real password, make up a dummy
  308. user.password = Constants.EXTERNAL_ACCOUNT;
  309. user.accountType = getAccountType();
  310. // Get full name Attribute
  311. String displayName = settings.getString(Keys.realm.ldap.displayName, "");
  312. if (!StringUtils.isEmpty(displayName)) {
  313. // Replace embedded ${} with attributes
  314. if (displayName.contains("${")) {
  315. for (Attribute userAttribute : userEntry.getAttributes())
  316. displayName = StringUtils.replace(displayName, "${" + userAttribute.getName() + "}", userAttribute.getValue());
  317. user.displayName = displayName;
  318. } else {
  319. Attribute attribute = userEntry.getAttribute(displayName);
  320. if (attribute != null && attribute.hasValue()) {
  321. user.displayName = attribute.getValue();
  322. }
  323. }
  324. }
  325. // Get email address Attribute
  326. String email = settings.getString(Keys.realm.ldap.email, "");
  327. if (!StringUtils.isEmpty(email)) {
  328. if (email.contains("${")) {
  329. for (Attribute userAttribute : userEntry.getAttributes())
  330. email = StringUtils.replace(email, "${" + userAttribute.getName() + "}", userAttribute.getValue());
  331. user.emailAddress = email;
  332. } else {
  333. Attribute attribute = userEntry.getAttribute(email);
  334. if (attribute != null && attribute.hasValue()) {
  335. user.emailAddress = attribute.getValue();
  336. }
  337. }
  338. }
  339. }
  340. private void getTeamsFromLdap(LDAPConnection ldapConnection, String simpleUsername, SearchResultEntry loggingInUser, UserModel user) {
  341. String loggingInUserDN = loggingInUser.getDN();
  342. user.teams.clear(); // Clear the users team memberships - we're going to get them from LDAP
  343. String groupBase = settings.getString(Keys.realm.ldap.groupBase, "");
  344. String groupMemberPattern = settings.getString(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))");
  345. groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", escapeLDAPSearchFilter(loggingInUserDN));
  346. groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
  347. // Fill in attributes into groupMemberPattern
  348. for (Attribute userAttribute : loggingInUser.getAttributes())
  349. groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue()));
  350. SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, true, groupMemberPattern, Arrays.asList("cn"));
  351. if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) {
  352. for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) {
  353. SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i);
  354. String teamName = teamEntry.getAttribute("cn").getValue();
  355. TeamModel teamModel = getTeamModel(teamName);
  356. if (teamModel == null)
  357. teamModel = createTeamFromLdap(teamEntry);
  358. user.teams.add(teamModel);
  359. teamModel.addUser(user.getName());
  360. }
  361. }
  362. }
  363. private TeamModel createTeamFromLdap(SearchResultEntry teamEntry) {
  364. TeamModel answer = new TeamModel(teamEntry.getAttributeValue("cn"));
  365. // potentially retrieve other attributes here in the future
  366. return answer;
  367. }
  368. private SearchResult doSearch(LDAPConnection ldapConnection, String base, String filter) {
  369. try {
  370. return ldapConnection.search(base, SearchScope.SUB, filter);
  371. } catch (LDAPSearchException e) {
  372. logger.error("Problem Searching LDAP", e);
  373. return null;
  374. }
  375. }
  376. private SearchResult doSearch(LDAPConnection ldapConnection, String base, boolean dereferenceAliases, String filter, List<String> attributes) {
  377. try {
  378. SearchRequest searchRequest = new SearchRequest(base, SearchScope.SUB, filter);
  379. if ( dereferenceAliases ) {
  380. searchRequest.setDerefPolicy(DereferencePolicy.SEARCHING);
  381. }
  382. if (attributes != null) {
  383. searchRequest.setAttributes(attributes);
  384. }
  385. return ldapConnection.search(searchRequest);
  386. } catch (LDAPSearchException e) {
  387. logger.error("Problem Searching LDAP", e);
  388. return null;
  389. } catch (LDAPException e) {
  390. logger.error("Problem creating LDAP search", e);
  391. return null;
  392. }
  393. }
  394. private boolean isAuthenticated(LDAPConnection ldapConnection, String userDn, String password) {
  395. try {
  396. // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN
  397. ldapConnection.bind(userDn, password);
  398. return true;
  399. } catch (LDAPException e) {
  400. logger.error("Error authenticating user", e);
  401. return false;
  402. }
  403. }
  404. @Override
  405. public List<String> getAllUsernames() {
  406. synchronizeLdapUsers();
  407. return super.getAllUsernames();
  408. }
  409. @Override
  410. public List<UserModel> getAllUsers() {
  411. synchronizeLdapUsers();
  412. return super.getAllUsers();
  413. }
  414. /**
  415. * Returns a simple username without any domain prefixes.
  416. *
  417. * @param username
  418. * @return a simple username
  419. */
  420. protected String getSimpleUsername(String username) {
  421. int lastSlash = username.lastIndexOf('\\');
  422. if (lastSlash > -1) {
  423. username = username.substring(lastSlash + 1);
  424. }
  425. return username;
  426. }
  427. // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
  428. public static final String escapeLDAPSearchFilter(String filter) {
  429. StringBuilder sb = new StringBuilder();
  430. for (int i = 0; i < filter.length(); i++) {
  431. char curChar = filter.charAt(i);
  432. switch (curChar) {
  433. case '\\':
  434. sb.append("\\5c");
  435. break;
  436. case '*':
  437. sb.append("\\2a");
  438. break;
  439. case '(':
  440. sb.append("\\28");
  441. break;
  442. case ')':
  443. sb.append("\\29");
  444. break;
  445. case '\u0000':
  446. sb.append("\\00");
  447. break;
  448. default:
  449. sb.append(curChar);
  450. }
  451. }
  452. return sb.toString();
  453. }
  454. }