| | |
| | | import java.net.URI; |
| | | import java.net.URISyntaxException; |
| | | import java.security.GeneralSecurityException; |
| | | import java.text.MessageFormat; |
| | | import java.util.Arrays; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | |
| | | import java.util.concurrent.Executors; |
| | | import java.util.concurrent.ScheduledExecutorService; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.concurrent.atomic.AtomicLong; |
| | | |
| | | import com.gitblit.Constants; |
| | | import com.gitblit.Constants.AccountType; |
| | | import com.gitblit.Constants.Role; |
| | | import com.gitblit.Keys; |
| | | import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider; |
| | | import com.gitblit.models.TeamModel; |
| | |
| | | */ |
| | | public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider { |
| | | |
| | | private final AtomicLong lastLdapUserSync = new AtomicLong(0L); |
| | | private final ScheduledExecutorService scheduledExecutorService; |
| | | |
| | | public LdapAuthProvider() { |
| | | super("ldap"); |
| | | |
| | | scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); |
| | | } |
| | | |
| | | private long getSynchronizationPeriodInMilliseconds(String name) { |
| | | final String cacheDuration = settings.getString(name, "2 MINUTES"); |
| | | private long getSynchronizationPeriodInMilliseconds() { |
| | | String period = settings.getString(Keys.realm.ldap.syncPeriod, null); |
| | | if (StringUtils.isEmpty(period)) { |
| | | period = settings.getString("realm.ldap.ldapCachePeriod", null); |
| | | if (StringUtils.isEmpty(period)) { |
| | | period = "5 MINUTES"; |
| | | } else { |
| | | logger.warn("realm.ldap.ldapCachePeriod is obsolete!"); |
| | | logger.warn(MessageFormat.format("Please set {0}={1} in gitblit.properties!", Keys.realm.ldap.syncPeriod, period)); |
| | | settings.overrideSetting(Keys.realm.ldap.syncPeriod, period); |
| | | } |
| | | } |
| | | |
| | | try { |
| | | final String[] s = cacheDuration.split(" ", 2); |
| | | final String[] s = period.split(" ", 2); |
| | | long duration = Math.abs(Long.parseLong(s[0])); |
| | | TimeUnit timeUnit = TimeUnit.valueOf(s[1]); |
| | | return timeUnit.toMillis(duration); |
| | | } catch (RuntimeException ex) { |
| | | throw new IllegalArgumentException(name + " must have format '<long> <TimeUnit>' where <TimeUnit> is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'"); |
| | | throw new IllegalArgumentException(Keys.realm.ldap.syncPeriod + " must have format '<long> <TimeUnit>' where <TimeUnit> is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void setup() { |
| | | synchronizeLdapUsers(); |
| | | configureLdapSyncService(); |
| | | } |
| | | |
| | | public void synchronizeWithLdapService() { |
| | | synchronizeLdapUsers(); |
| | | configureSyncService(); |
| | | } |
| | | |
| | | protected synchronized void synchronizeLdapUsers() { |
| | | final boolean enabled = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.enable, false); |
| | | if (enabled) { |
| | | if (System.currentTimeMillis() > (lastLdapUserSync.get() + getSynchronizationPeriodInMilliseconds(Keys.realm.ldap.ldapCachePeriod))) { |
| | | logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server)); |
| | | final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.removeDeleted, true); |
| | | LDAPConnection ldapConnection = getLdapConnection(); |
| | | if (ldapConnection != null) { |
| | | try { |
| | | String accountBase = settings.getString(Keys.realm.ldap.accountBase, ""); |
| | | String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid"); |
| | | String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))"); |
| | | accountPattern = StringUtils.replace(accountPattern, "${username}", "*"); |
| | | @Override |
| | | public void stop() { |
| | | scheduledExecutorService.shutdownNow(); |
| | | } |
| | | |
| | | SearchResult result = doSearch(ldapConnection, accountBase, accountPattern); |
| | | if (result != null && result.getEntryCount() > 0) { |
| | | final Map<String, UserModel> ldapUsers = new HashMap<String, UserModel>(); |
| | | public synchronized void sync() { |
| | | final boolean enabled = settings.getBoolean(Keys.realm.ldap.synchronize, false); |
| | | if (enabled) { |
| | | logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server)); |
| | | final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.removeDeletedUsers, true); |
| | | LDAPConnection ldapConnection = getLdapConnection(); |
| | | if (ldapConnection != null) { |
| | | try { |
| | | String accountBase = settings.getString(Keys.realm.ldap.accountBase, ""); |
| | | String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid"); |
| | | String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))"); |
| | | accountPattern = StringUtils.replace(accountPattern, "${username}", "*"); |
| | | |
| | | for (SearchResultEntry loggingInUser : result.getSearchEntries()) { |
| | | SearchResult result = doSearch(ldapConnection, accountBase, accountPattern); |
| | | if (result != null && result.getEntryCount() > 0) { |
| | | final Map<String, UserModel> ldapUsers = new HashMap<String, UserModel>(); |
| | | |
| | | final String username = loggingInUser.getAttribute(uidAttribute).getValue(); |
| | | logger.debug("LDAP synchronizing: " + username); |
| | | for (SearchResultEntry loggingInUser : result.getSearchEntries()) { |
| | | Attribute uid = loggingInUser.getAttribute(uidAttribute); |
| | | if (uid == null) { |
| | | logger.error("Can not synchronize with LDAP, missing \"{}\" attribute", uidAttribute); |
| | | continue; |
| | | } |
| | | final String username = uid.getValue(); |
| | | logger.debug("LDAP synchronizing: " + username); |
| | | |
| | | UserModel user = userManager.getUserModel(username); |
| | | if (user == null) { |
| | | user = new UserModel(username); |
| | | } |
| | | UserModel user = userManager.getUserModel(username); |
| | | if (user == null) { |
| | | user = new UserModel(username); |
| | | } |
| | | |
| | | if (!supportsTeamMembershipChanges()) { |
| | | getTeamsFromLdap(ldapConnection, username, loggingInUser, user); |
| | | } |
| | | if (!supportsTeamMembershipChanges()) { |
| | | getTeamsFromLdap(ldapConnection, username, loggingInUser, user); |
| | | } |
| | | |
| | | // Get User Attributes |
| | | setUserAttributes(user, loggingInUser); |
| | | // Get User Attributes |
| | | setUserAttributes(user, loggingInUser); |
| | | |
| | | // store in map |
| | | ldapUsers.put(username.toLowerCase(), user); |
| | | } |
| | | // store in map |
| | | ldapUsers.put(username.toLowerCase(), user); |
| | | } |
| | | |
| | | if (deleteRemovedLdapUsers) { |
| | | logger.debug("detecting removed LDAP users..."); |
| | | if (deleteRemovedLdapUsers) { |
| | | logger.debug("detecting removed LDAP users..."); |
| | | |
| | | for (UserModel userModel : userManager.getAllUsers()) { |
| | | if (Constants.EXTERNAL_ACCOUNT.equals(userModel.password)) { |
| | | if (!ldapUsers.containsKey(userModel.username)) { |
| | | logger.info("deleting removed LDAP user " + userModel.username + " from user service"); |
| | | userManager.deleteUser(userModel.username); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | for (UserModel userModel : userManager.getAllUsers()) { |
| | | if (AccountType.LDAP == userModel.accountType) { |
| | | if (!ldapUsers.containsKey(userModel.username)) { |
| | | logger.info("deleting removed LDAP user " + userModel.username + " from user service"); |
| | | userManager.deleteUser(userModel.username); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | userManager.updateUserModels(ldapUsers.values()); |
| | | userManager.updateUserModels(ldapUsers.values()); |
| | | |
| | | if (!supportsTeamMembershipChanges()) { |
| | | final Map<String, TeamModel> userTeams = new HashMap<String, TeamModel>(); |
| | | for (UserModel user : ldapUsers.values()) { |
| | | for (TeamModel userTeam : user.teams) { |
| | | userTeams.put(userTeam.name, userTeam); |
| | | } |
| | | } |
| | | userManager.updateTeamModels(userTeams.values()); |
| | | } |
| | | } |
| | | if (!supportsTeamMembershipChanges()) { |
| | | getEmptyTeamsFromLdap(ldapConnection); |
| | | } |
| | | lastLdapUserSync.set(System.currentTimeMillis()); |
| | | } finally { |
| | | ldapConnection.close(); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | if (!supportsTeamMembershipChanges()) { |
| | | final Map<String, TeamModel> userTeams = new HashMap<String, TeamModel>(); |
| | | for (UserModel user : ldapUsers.values()) { |
| | | for (TeamModel userTeam : user.teams) { |
| | | userTeams.put(userTeam.name, userTeam); |
| | | } |
| | | } |
| | | userManager.updateTeamModels(userTeams.values()); |
| | | } |
| | | } |
| | | if (!supportsTeamMembershipChanges()) { |
| | | getEmptyTeamsFromLdap(ldapConnection); |
| | | } |
| | | } finally { |
| | | ldapConnection.close(); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private LDAPConnection getLdapConnection() { |
| | | try { |
| | |
| | | return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.email, "")); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * If the LDAP server will maintain team memberships then LdapUserService |
| | | * will not allow team membership changes. In this scenario all team |
| | |
| | | @Override |
| | | public boolean supportsTeamMembershipChanges() { |
| | | return !settings.getBoolean(Keys.realm.ldap.maintainTeams, false); |
| | | } |
| | | |
| | | @Override |
| | | public boolean supportsRoleChanges(UserModel user, Role role) { |
| | | if (Role.ADMIN == role) { |
| | | if (!supportsTeamMembershipChanges()) { |
| | | List<String> admins = settings.getStrings(Keys.realm.ldap.admins); |
| | | if (admins.contains(user.username)) { |
| | | return false; |
| | | } |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public boolean supportsRoleChanges(TeamModel team, Role role) { |
| | | if (Role.ADMIN == role) { |
| | | if (!supportsTeamMembershipChanges()) { |
| | | List<String> admins = settings.getStrings(Keys.realm.ldap.admins); |
| | | if (admins.contains("@" + team.name)) { |
| | | return false; |
| | | } |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | |
| | | LDAPConnection ldapConnection = getLdapConnection(); |
| | | if (ldapConnection != null) { |
| | | try { |
| | | boolean alreadyAuthenticated = false; |
| | | |
| | | String bindPattern = settings.getString(Keys.realm.ldap.bindpattern, ""); |
| | | if (!StringUtils.isEmpty(bindPattern)) { |
| | | try { |
| | | String bindUser = StringUtils.replace(bindPattern, "${username}", escapeLDAPSearchFilter(simpleUsername)); |
| | | ldapConnection.bind(bindUser, new String(password)); |
| | | |
| | | alreadyAuthenticated = true; |
| | | } catch (LDAPException e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | // Find the logging in user's DN |
| | | String accountBase = settings.getString(Keys.realm.ldap.accountBase, ""); |
| | | String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))"); |
| | |
| | | SearchResultEntry loggingInUser = result.getSearchEntries().get(0); |
| | | String loggingInUserDN = loggingInUser.getDN(); |
| | | |
| | | if (isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) { |
| | | if (alreadyAuthenticated || isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) { |
| | | logger.debug("LDAP authenticated: " + username); |
| | | |
| | | UserModel user = null; |
| | |
| | | Attribute attribute = userEntry.getAttribute(email); |
| | | if (attribute != null && attribute.hasValue()) { |
| | | user.emailAddress = attribute.getValue(); |
| | | } else { |
| | | // issue-456/ticket-134 |
| | | // allow LDAP to delete an email address |
| | | user.emailAddress = null; |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | private void getEmptyTeamsFromLdap(LDAPConnection ldapConnection) { |
| | | logger.info("Start fetching empty teams from ldap."); |
| | | String groupBase = settings.getString(Keys.realm.ldap.groupBase, ""); |
| | | String groupMemberPattern = settings.getString(Keys.realm.ldap.groupEmptyMemberPattern, "(&(objectClass=group)(!(member=*)))"); |
| | | |
| | |
| | | SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i); |
| | | if (!teamEntry.hasAttribute("member")) { |
| | | String teamName = teamEntry.getAttribute("cn").getValue(); |
| | | |
| | | |
| | | TeamModel teamModel = userManager.getTeamModel(teamName); |
| | | if (teamModel == null) { |
| | | teamModel = createTeamFromLdap(teamEntry); |
| | |
| | | } |
| | | } |
| | | } |
| | | logger.info("Finished fetching empty teams from ldap."); |
| | | } |
| | | |
| | | private TeamModel createTeamFromLdap(SearchResultEntry teamEntry) { |
| | |
| | | return sb.toString(); |
| | | } |
| | | |
| | | private void configureLdapSyncService() { |
| | | logger.info("Start configuring ldap sync service"); |
| | | private void configureSyncService() { |
| | | LdapSyncService ldapSyncService = new LdapSyncService(settings, this); |
| | | if (ldapSyncService.isReady()) { |
| | | long ldapSyncPeriod = getSynchronizationPeriodInMilliseconds(Keys.realm.ldap.synchronizeUsers.ldapSyncPeriod); |
| | | long ldapCachePeriod = getSynchronizationPeriodInMilliseconds(Keys.realm.ldap.synchronizeUsers.ldapSyncPeriod); |
| | | if (ldapSyncPeriod < ldapCachePeriod) { |
| | | ldapSyncPeriod = ldapCachePeriod; |
| | | } |
| | | long ldapSyncPeriod = getSynchronizationPeriodInMilliseconds(); |
| | | int delay = 1; |
| | | ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); |
| | | logger.info("Ldap sync service will update users and groups every {} minutes.", |
| | | TimeUnit.MILLISECONDS.toMinutes(ldapSyncPeriod)); |
| | | scheduledExecutorService.scheduleAtFixedRate(ldapSyncService, delay, ldapSyncPeriod, TimeUnit.MILLISECONDS); |
| | | logger.info("Ldap sync service will update user and groups every {} minutes.", ldapSyncPeriod); |
| | | logger.info("Next scheduled ldap sync is in {} minutes", delay); |
| | | } else { |
| | | logger.info("Ldap sync service is disabled."); |
| | | } |
| | | logger.info("Finished configuring ldap sync service"); |
| | | } |
| | | |
| | | } |