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.

LdapAuthProvider.java 18KB

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