選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

LdapUserService.java 18KB

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