diff options
Diffstat (limited to 'src/main/java/com/gitblit/auth')
-rw-r--r-- | src/main/java/com/gitblit/auth/AuthenticationProvider.java | 182 | ||||
-rw-r--r-- | src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java | 276 | ||||
-rw-r--r-- | src/main/java/com/gitblit/auth/LdapAuthProvider.java | 508 | ||||
-rw-r--r-- | src/main/java/com/gitblit/auth/PAMAuthProvider.java | 126 | ||||
-rw-r--r-- | src/main/java/com/gitblit/auth/RedmineAuthProvider.java | 186 | ||||
-rw-r--r-- | src/main/java/com/gitblit/auth/SalesforceAuthProvider.java | 128 | ||||
-rw-r--r-- | src/main/java/com/gitblit/auth/WindowsAuthProvider.java | 177 |
7 files changed, 1583 insertions, 0 deletions
diff --git a/src/main/java/com/gitblit/auth/AuthenticationProvider.java b/src/main/java/com/gitblit/auth/AuthenticationProvider.java new file mode 100644 index 00000000..b8aaf079 --- /dev/null +++ b/src/main/java/com/gitblit/auth/AuthenticationProvider.java @@ -0,0 +1,182 @@ +/* + * Copyright 2013 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.auth; + +import java.io.File; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.Constants.AccountType; +import com.gitblit.IStoredSettings; +import com.gitblit.manager.IRuntimeManager; +import com.gitblit.manager.IUserManager; +import com.gitblit.models.TeamModel; +import com.gitblit.models.UserModel; + +public abstract class AuthenticationProvider { + + public static NullProvider NULL_PROVIDER = new NullProvider(); + + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + protected final String serviceName; + + protected File baseFolder; + + protected IStoredSettings settings; + + protected IRuntimeManager runtimeManager; + + protected IUserManager userManager; + + protected AuthenticationProvider(String serviceName) { + this.serviceName = serviceName; + } + + /** + * Returns the file object for the specified configuration key. + * + * @return the file + */ + public File getFileOrFolder(String key, String defaultFileOrFolder) { + return runtimeManager.getFileOrFolder(key, defaultFileOrFolder); + } + + public final void setup(IRuntimeManager runtimeManager, IUserManager userManager) { + this.baseFolder = runtimeManager.getBaseFolder(); + this.settings = runtimeManager.getSettings(); + this.runtimeManager = runtimeManager; + this.userManager = userManager; + setup(); + } + + public String getServiceName() { + return serviceName; + } + + protected void updateUser(UserModel userModel) { + // TODO implement user model change detection + // account for new user and revised user + + // username + // displayname + // email address + // cookie + + userManager.updateUserModel(userModel); + } + + protected void updateTeam(TeamModel teamModel) { + // TODO implement team model change detection + // account for new team and revised team + + // memberships + + userManager.updateTeamModel(teamModel); + } + + public abstract void setup(); + + public abstract UserModel authenticate(String username, char[] password); + + public abstract AccountType getAccountType(); + + /** + * Does the user service support changes to credentials? + * + * @return true or false + * @since 1.0.0 + */ + public abstract boolean supportsCredentialChanges(); + + /** + * Returns true if the user's display name can be changed. + * + * @param user + * @return true if the user service supports display name changes + */ + public abstract boolean supportsDisplayNameChanges(); + + /** + * Returns true if the user's email address can be changed. + * + * @param user + * @return true if the user service supports email address changes + */ + public abstract boolean supportsEmailAddressChanges(); + + /** + * Returns true if the user's team memberships can be changed. + * + * @param user + * @return true if the user service supports team membership changes + */ + public abstract boolean supportsTeamMembershipChanges(); + + @Override + public String toString() { + return getServiceName() + " (" + getClass().getName() + ")"; + } + + public abstract static class UsernamePasswordAuthenticationProvider extends AuthenticationProvider { + protected UsernamePasswordAuthenticationProvider(String serviceName) { + super(serviceName); + } + } + + public static class NullProvider extends AuthenticationProvider { + + protected NullProvider() { + super("NULL"); + } + + @Override + public void setup() { + + } + + @Override + public UserModel authenticate(String username, char[] password) { + return null; + } + + @Override + public AccountType getAccountType() { + return AccountType.LOCAL; + } + + @Override + public boolean supportsCredentialChanges() { + return false; + } + + @Override + public boolean supportsDisplayNameChanges() { + return false; + } + + @Override + public boolean supportsEmailAddressChanges() { + return false; + } + + @Override + public boolean supportsTeamMembershipChanges() { + return false; + } + } +} diff --git a/src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java b/src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java new file mode 100644 index 00000000..559a0fa0 --- /dev/null +++ b/src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java @@ -0,0 +1,276 @@ +/* + * Copyright 2013 Florian Zschocke + * Copyright 2013 gitblit.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.auth; + +import java.io.File; +import java.io.FileInputStream; +import java.text.MessageFormat; +import java.util.Map; +import java.util.Scanner; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.digest.Crypt; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.codec.digest.Md5Crypt; + +import com.gitblit.Constants; +import com.gitblit.Constants.AccountType; +import com.gitblit.Keys; +import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider; +import com.gitblit.models.UserModel; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.StringUtils; + + +/** + * Implementation of a user service using an Apache htpasswd file for authentication. + * + * This user service implement custom authentication using entries in a file created + * by the 'htpasswd' program of an Apache web server. All possible output + * options of the 'htpasswd' program version 2.2 are supported: + * plain text (only on Windows and Netware), + * glibc crypt() (not on Windows and NetWare), + * Apache MD5 (apr1), + * unsalted SHA-1. + * + * Configuration options: + * realm.htpasswd.backingUserService - Specify the backing user service that is used + * to keep the user data other than the password. + * The default is '${baseFolder}/users.conf'. + * realm.htpasswd.userfile - The text file with the htpasswd entries to be used for + * authentication. + * The default is '${baseFolder}/htpasswd'. + * realm.htpasswd.overrideLocalAuthentication - Specify if local accounts are overwritten + * when authentication matches for an + * external account. + * + * @author Florian Zschocke + * + */ +public class HtpasswdAuthProvider extends UsernamePasswordAuthenticationProvider { + + private static final String KEY_HTPASSWD_FILE = Keys.realm.htpasswd.userfile; + private static final String DEFAULT_HTPASSWD_FILE = "${baseFolder}/htpasswd"; + + private static final String KEY_SUPPORT_PLAINTEXT_PWD = "realm.htpasswd.supportPlaintextPasswords"; + + private boolean supportPlainTextPwd; + + private File htpasswdFile; + + private final Map<String, String> htUsers = new ConcurrentHashMap<String, String>(); + + private volatile long lastModified; + + public HtpasswdAuthProvider() { + super("htpasswd"); + } + + /** + * Setup the user service. + * + * The HtpasswdUserService extends the GitblitUserService and is thus + * backed by the available user services provided by the GitblitUserService. + * In addition the setup tries to read and parse the htpasswd file to be used + * for authentication. + * + * @param settings + * @since 0.7.0 + */ + @Override + public void setup() { + String os = System.getProperty("os.name").toLowerCase(); + if (os.startsWith("windows") || os.startsWith("netware")) { + supportPlainTextPwd = true; + } else { + supportPlainTextPwd = false; + } + read(); + logger.debug("Read " + htUsers.size() + " users from htpasswd file: " + this.htpasswdFile); + } + + @Override + public boolean supportsCredentialChanges() { + return false; + } + + @Override + public boolean supportsDisplayNameChanges() { + return true; + } + + @Override + public boolean supportsEmailAddressChanges() { + return true; + } + + @Override + public boolean supportsTeamMembershipChanges() { + return true; + } + + /** + * Authenticate a user based on a username and password. + * + * If the account is determined to be a local account, authentication + * will be done against the locally stored password. + * Otherwise, the configured htpasswd file is read. All current output options + * of htpasswd are supported: clear text, crypt(), Apache MD5 and unsalted SHA-1. + * + * @param username + * @param password + * @return a user object or null + */ + @Override + public UserModel authenticate(String username, char[] password) { + read(); + String storedPwd = htUsers.get(username); + if (storedPwd != null) { + boolean authenticated = false; + final String passwd = new String(password); + + // test Apache MD5 variant encrypted password + if (storedPwd.startsWith("$apr1$")) { + if (storedPwd.equals(Md5Crypt.apr1Crypt(passwd, storedPwd))) { + logger.debug("Apache MD5 encoded password matched for user '" + username + "'"); + authenticated = true; + } + } + // test unsalted SHA password + else if (storedPwd.startsWith("{SHA}")) { + String passwd64 = Base64.encodeBase64String(DigestUtils.sha1(passwd)); + if (storedPwd.substring("{SHA}".length()).equals(passwd64)) { + logger.debug("Unsalted SHA-1 encoded password matched for user '" + username + "'"); + authenticated = true; + } + } + // test libc crypt() encoded password + else if (supportCryptPwd() && storedPwd.equals(Crypt.crypt(passwd, storedPwd))) { + logger.debug("Libc crypt encoded password matched for user '" + username + "'"); + authenticated = true; + } + // test clear text + else if (supportPlaintextPwd() && storedPwd.equals(passwd)){ + logger.debug("Clear text password matched for user '" + username + "'"); + authenticated = true; + } + + + if (authenticated) { + logger.debug("Htpasswd authenticated: " + username); + + UserModel curr = userManager.getUserModel(username); + UserModel user; + if (curr == null) { + // create user object for new authenticated user + user = new UserModel(username); + } else { + user = curr; + } + + // create a user cookie + if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) { + user.cookie = StringUtils.getSHA1(user.username + passwd); + } + + // Set user attributes, hide password from backing user service. + user.password = Constants.EXTERNAL_ACCOUNT; + user.accountType = getAccountType(); + + // Push the looked up values to backing file + updateUser(user); + + return user; + } + } + + return null; + } + + /** + * Get the account type used for this user service. + * + * @return AccountType.HTPASSWD + */ + @Override + public AccountType getAccountType() { + return AccountType.HTPASSWD; + } + + /** + * Reads the realm file and rebuilds the in-memory lookup tables. + */ + protected synchronized void read() { + boolean forceReload = false; + File file = getFileOrFolder(KEY_HTPASSWD_FILE, DEFAULT_HTPASSWD_FILE); + if (!file.equals(htpasswdFile)) { + this.htpasswdFile = file; + this.htUsers.clear(); + forceReload = true; + } + + if (htpasswdFile.exists() && (forceReload || (htpasswdFile.lastModified() != lastModified))) { + lastModified = htpasswdFile.lastModified(); + htUsers.clear(); + + Pattern entry = Pattern.compile("^([^:]+):(.+)"); + + Scanner scanner = null; + try { + scanner = new Scanner(new FileInputStream(htpasswdFile)); + while (scanner.hasNextLine()) { + String line = scanner.nextLine().trim(); + if (!line.isEmpty() && !line.startsWith("#")) { + Matcher m = entry.matcher(line); + if (m.matches()) { + htUsers.put(m.group(1), m.group(2)); + } + } + } + } catch (Exception e) { + logger.error(MessageFormat.format("Failed to read {0}", htpasswdFile), e); + } finally { + if (scanner != null) { + scanner.close(); + } + } + } + } + + private boolean supportPlaintextPwd() { + return this.settings.getBoolean(KEY_SUPPORT_PLAINTEXT_PWD, supportPlainTextPwd); + } + + private boolean supportCryptPwd() { + return !supportPlaintextPwd(); + } + + /* + * Method only used for unit tests. Return number of users read from htpasswd file. + */ + public int getNumberHtpasswdUsers() { + return this.htUsers.size(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + ((htpasswdFile != null) ? htpasswdFile.getAbsolutePath() : "null") + ")"; + } +} diff --git a/src/main/java/com/gitblit/auth/LdapAuthProvider.java b/src/main/java/com/gitblit/auth/LdapAuthProvider.java new file mode 100644 index 00000000..7a6b74df --- /dev/null +++ b/src/main/java/com/gitblit/auth/LdapAuthProvider.java @@ -0,0 +1,508 @@ +/* + * Copyright 2012 John Crygier + * Copyright 2012 gitblit.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.auth; + +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import com.gitblit.Constants; +import com.gitblit.Constants.AccountType; +import com.gitblit.Keys; +import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider; +import com.gitblit.models.TeamModel; +import com.gitblit.models.UserModel; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.StringUtils; +import com.unboundid.ldap.sdk.Attribute; +import com.unboundid.ldap.sdk.DereferencePolicy; +import com.unboundid.ldap.sdk.ExtendedResult; +import com.unboundid.ldap.sdk.LDAPConnection; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.LDAPSearchException; +import com.unboundid.ldap.sdk.ResultCode; +import com.unboundid.ldap.sdk.SearchRequest; +import com.unboundid.ldap.sdk.SearchResult; +import com.unboundid.ldap.sdk.SearchResultEntry; +import com.unboundid.ldap.sdk.SearchScope; +import com.unboundid.ldap.sdk.SimpleBindRequest; +import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest; +import com.unboundid.util.ssl.SSLUtil; +import com.unboundid.util.ssl.TrustAllTrustManager; + +/** + * Implementation of an LDAP user service. + * + * @author John Crygier + */ +public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider { + + private AtomicLong lastLdapUserSync = new AtomicLong(0L); + + public LdapAuthProvider() { + super("ldap"); + } + + private long getSynchronizationPeriod() { + final String cacheDuration = settings.getString(Keys.realm.ldap.ldapCachePeriod, "2 MINUTES"); + try { + final String[] s = cacheDuration.split(" ", 2); + long duration = Long.parseLong(s[0]); + TimeUnit timeUnit = TimeUnit.valueOf(s[1]); + return timeUnit.toMillis(duration); + } catch (RuntimeException ex) { + throw new IllegalArgumentException(Keys.realm.ldap.ldapCachePeriod + " must have format '<long> <TimeUnit>' where <TimeUnit> is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'"); + } + } + + @Override + public void setup() { + synchronizeLdapUsers(); + } + + protected synchronized void synchronizeLdapUsers() { + final boolean enabled = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.enable, false); + if (enabled) { + if (System.currentTimeMillis() > (lastLdapUserSync.get() + getSynchronizationPeriod())) { + 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}", "*"); + + SearchResult result = doSearch(ldapConnection, accountBase, accountPattern); + if (result != null && result.getEntryCount() > 0) { + final Map<String, UserModel> ldapUsers = new HashMap<String, UserModel>(); + + for (SearchResultEntry loggingInUser : result.getSearchEntries()) { + + final String username = loggingInUser.getAttribute(uidAttribute).getValue(); + logger.debug("LDAP synchronizing: " + username); + + UserModel user = userManager.getUserModel(username); + if (user == null) { + user = new UserModel(username); + } + + if (!supportsTeamMembershipChanges()) { + getTeamsFromLdap(ldapConnection, username, loggingInUser, user); + } + + // Get User Attributes + setUserAttributes(user, loggingInUser); + + // store in map + ldapUsers.put(username.toLowerCase(), user); + } + + 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); + } + } + } + } + + 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()); + } + } + lastLdapUserSync.set(System.currentTimeMillis()); + } finally { + ldapConnection.close(); + } + } + } + } + } + + private LDAPConnection getLdapConnection() { + try { + + URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server)); + String ldapHost = ldapUrl.getHost(); + int ldapPort = ldapUrl.getPort(); + String bindUserName = settings.getString(Keys.realm.ldap.username, ""); + String bindPassword = settings.getString(Keys.realm.ldap.password, ""); + + + LDAPConnection conn; + if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) { + // SSL + SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager()); + conn = new LDAPConnection(sslUtil.createSSLSocketFactory()); + } else if (ldapUrl.getScheme().equalsIgnoreCase("ldap") || ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) { + // no encryption or StartTLS + conn = new LDAPConnection(); + } else { + logger.error("Unsupported LDAP URL scheme: " + ldapUrl.getScheme()); + return null; + } + + conn.connect(ldapHost, ldapPort); + + if (ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) { + SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager()); + ExtendedResult extendedResult = conn.processExtendedOperation( + new StartTLSExtendedRequest(sslUtil.createSSLContext())); + if (extendedResult.getResultCode() != ResultCode.SUCCESS) { + throw new LDAPException(extendedResult.getResultCode()); + } + } + + if (!StringUtils.isEmpty(bindUserName) || !StringUtils.isEmpty(bindPassword)) { + conn.bind(new SimpleBindRequest(bindUserName, bindPassword)); + } + + return conn; + + } catch (URISyntaxException e) { + logger.error("Bad LDAP URL, should be in the form: ldap(s|+tls)://<server>:<port>", e); + } catch (GeneralSecurityException e) { + logger.error("Unable to create SSL Connection", e); + } catch (LDAPException e) { + logger.error("Error Connecting to LDAP", e); + } + + return null; + } + + /** + * Credentials are defined in the LDAP server and can not be manipulated + * from Gitblit. + * + * @return false + * @since 1.0.0 + */ + @Override + public boolean supportsCredentialChanges() { + return false; + } + + /** + * If no displayName pattern is defined then Gitblit can manage the display name. + * + * @return true if Gitblit can manage the user display name + * @since 1.0.0 + */ + @Override + public boolean supportsDisplayNameChanges() { + return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.displayName, "")); + } + + /** + * If no email pattern is defined then Gitblit can manage the email address. + * + * @return true if Gitblit can manage the user email address + * @since 1.0.0 + */ + @Override + public boolean supportsEmailAddressChanges() { + 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 + * changes must be made on the LDAP server by the LDAP administrator. + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsTeamMembershipChanges() { + return !settings.getBoolean(Keys.realm.ldap.maintainTeams, false); + } + + @Override + public AccountType getAccountType() { + return AccountType.LDAP; + } + + @Override + public UserModel authenticate(String username, char[] password) { + String simpleUsername = getSimpleUsername(username); + + LDAPConnection ldapConnection = getLdapConnection(); + if (ldapConnection != null) { + try { + // 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}))"); + accountPattern = StringUtils.replace(accountPattern, "${username}", escapeLDAPSearchFilter(simpleUsername)); + + SearchResult result = doSearch(ldapConnection, accountBase, accountPattern); + if (result != null && result.getEntryCount() == 1) { + SearchResultEntry loggingInUser = result.getSearchEntries().get(0); + String loggingInUserDN = loggingInUser.getDN(); + + if (isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) { + logger.debug("LDAP authenticated: " + username); + + UserModel user = null; + synchronized (this) { + user = userManager.getUserModel(simpleUsername); + if (user == null) // create user object for new authenticated user + user = new UserModel(simpleUsername); + + // create a user cookie + if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) { + user.cookie = StringUtils.getSHA1(user.username + new String(password)); + } + + if (!supportsTeamMembershipChanges()) + getTeamsFromLdap(ldapConnection, simpleUsername, loggingInUser, user); + + // Get User Attributes + setUserAttributes(user, loggingInUser); + + // Push the ldap looked up values to backing file + updateUser(user); + + if (!supportsTeamMembershipChanges()) { + for (TeamModel userTeam : user.teams) + updateTeam(userTeam); + } + } + + return user; + } + } + } finally { + ldapConnection.close(); + } + } + return null; + } + + /** + * Set the admin attribute from team memberships retrieved from LDAP. + * If we are not storing teams in LDAP and/or we have not defined any + * administrator teams, then do not change the admin flag. + * + * @param user + */ + private void setAdminAttribute(UserModel user) { + if (!supportsTeamMembershipChanges()) { + List<String> admins = settings.getStrings(Keys.realm.ldap.admins); + // if we have defined administrative teams, then set admin flag + // otherwise leave admin flag unchanged + if (!ArrayUtils.isEmpty(admins)) { + user.canAdmin = false; + for (String admin : admins) { + if (admin.startsWith("@")) { // Team + if (user.getTeam(admin.substring(1)) != null) + user.canAdmin = true; + } else + if (user.getName().equalsIgnoreCase(admin)) + user.canAdmin = true; + } + } + } + } + + private void setUserAttributes(UserModel user, SearchResultEntry userEntry) { + // Is this user an admin? + setAdminAttribute(user); + + // Don't want visibility into the real password, make up a dummy + user.password = Constants.EXTERNAL_ACCOUNT; + user.accountType = getAccountType(); + + // Get full name Attribute + String displayName = settings.getString(Keys.realm.ldap.displayName, ""); + if (!StringUtils.isEmpty(displayName)) { + // Replace embedded ${} with attributes + if (displayName.contains("${")) { + for (Attribute userAttribute : userEntry.getAttributes()) + displayName = StringUtils.replace(displayName, "${" + userAttribute.getName() + "}", userAttribute.getValue()); + + user.displayName = displayName; + } else { + Attribute attribute = userEntry.getAttribute(displayName); + if (attribute != null && attribute.hasValue()) { + user.displayName = attribute.getValue(); + } + } + } + + // Get email address Attribute + String email = settings.getString(Keys.realm.ldap.email, ""); + if (!StringUtils.isEmpty(email)) { + if (email.contains("${")) { + for (Attribute userAttribute : userEntry.getAttributes()) + email = StringUtils.replace(email, "${" + userAttribute.getName() + "}", userAttribute.getValue()); + + user.emailAddress = email; + } else { + Attribute attribute = userEntry.getAttribute(email); + if (attribute != null && attribute.hasValue()) { + user.emailAddress = attribute.getValue(); + } + } + } + } + + private void getTeamsFromLdap(LDAPConnection ldapConnection, String simpleUsername, SearchResultEntry loggingInUser, UserModel user) { + String loggingInUserDN = loggingInUser.getDN(); + + user.teams.clear(); // Clear the users team memberships - we're going to get them from LDAP + String groupBase = settings.getString(Keys.realm.ldap.groupBase, ""); + String groupMemberPattern = settings.getString(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))"); + + groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", escapeLDAPSearchFilter(loggingInUserDN)); + groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", escapeLDAPSearchFilter(simpleUsername)); + + // Fill in attributes into groupMemberPattern + for (Attribute userAttribute : loggingInUser.getAttributes()) { + groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue())); + } + + SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, true, groupMemberPattern, Arrays.asList("cn")); + if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) { + for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) { + SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i); + String teamName = teamEntry.getAttribute("cn").getValue(); + + TeamModel teamModel = userManager.getTeamModel(teamName); + if (teamModel == null) { + teamModel = createTeamFromLdap(teamEntry); + } + + user.teams.add(teamModel); + teamModel.addUser(user.getName()); + } + } + } + + private TeamModel createTeamFromLdap(SearchResultEntry teamEntry) { + TeamModel answer = new TeamModel(teamEntry.getAttributeValue("cn")); + answer.accountType = getAccountType(); + // potentially retrieve other attributes here in the future + + return answer; + } + + private SearchResult doSearch(LDAPConnection ldapConnection, String base, String filter) { + try { + return ldapConnection.search(base, SearchScope.SUB, filter); + } catch (LDAPSearchException e) { + logger.error("Problem Searching LDAP", e); + + return null; + } + } + + private SearchResult doSearch(LDAPConnection ldapConnection, String base, boolean dereferenceAliases, String filter, List<String> attributes) { + try { + SearchRequest searchRequest = new SearchRequest(base, SearchScope.SUB, filter); + if (dereferenceAliases) { + searchRequest.setDerefPolicy(DereferencePolicy.SEARCHING); + } + if (attributes != null) { + searchRequest.setAttributes(attributes); + } + return ldapConnection.search(searchRequest); + + } catch (LDAPSearchException e) { + logger.error("Problem Searching LDAP", e); + + return null; + } catch (LDAPException e) { + logger.error("Problem creating LDAP search", e); + return null; + } + } + + private boolean isAuthenticated(LDAPConnection ldapConnection, String userDn, String password) { + try { + // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN + ldapConnection.bind(userDn, password); + return true; + } catch (LDAPException e) { + logger.error("Error authenticating user", e); + return false; + } + } + + /** + * Returns a simple username without any domain prefixes. + * + * @param username + * @return a simple username + */ + protected String getSimpleUsername(String username) { + int lastSlash = username.lastIndexOf('\\'); + if (lastSlash > -1) { + username = username.substring(lastSlash + 1); + } + + return username; + } + + // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java + public static final String escapeLDAPSearchFilter(String filter) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < filter.length(); i++) { + char curChar = filter.charAt(i); + switch (curChar) { + case '\\': + sb.append("\\5c"); + break; + case '*': + sb.append("\\2a"); + break; + case '(': + sb.append("\\28"); + break; + case ')': + sb.append("\\29"); + break; + case '\u0000': + sb.append("\\00"); + break; + default: + sb.append(curChar); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/com/gitblit/auth/PAMAuthProvider.java b/src/main/java/com/gitblit/auth/PAMAuthProvider.java new file mode 100644 index 00000000..bbc82d84 --- /dev/null +++ b/src/main/java/com/gitblit/auth/PAMAuthProvider.java @@ -0,0 +1,126 @@ +/* + * Copyright 2013 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.auth; + +import java.io.File; + +import org.jvnet.libpam.PAM; +import org.jvnet.libpam.PAMException; +import org.jvnet.libpam.impl.CLibrary; + +import com.gitblit.Constants; +import com.gitblit.Constants.AccountType; +import com.gitblit.Keys; +import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider; +import com.gitblit.models.UserModel; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.StringUtils; + +/** + * Implementation of PAM authentication for Linux/Unix/MacOSX. + * + * @author James Moger + */ +public class PAMAuthProvider extends UsernamePasswordAuthenticationProvider { + + public PAMAuthProvider() { + super("pam"); + } + + @Override + public void setup() { + // Try to identify the passwd database + String [] files = { "/etc/shadow", "/etc/master.passwd" }; + File passwdFile = null; + for (String name : files) { + File f = new File(name); + if (f.exists()) { + passwdFile = f; + break; + } + } + if (passwdFile == null) { + logger.error("PAM Authentication could not find a passwd database!"); + } else if (!passwdFile.canRead()) { + logger.error("PAM Authentication can not read passwd database {}! PAM authentications may fail!", passwdFile); + } + } + + @Override + public boolean supportsCredentialChanges() { + return false; + } + + @Override + public boolean supportsDisplayNameChanges() { + return true; + } + + @Override + public boolean supportsEmailAddressChanges() { + return true; + } + + @Override + public boolean supportsTeamMembershipChanges() { + return true; + } + + @Override + public AccountType getAccountType() { + return AccountType.PAM; + } + + @Override + public UserModel authenticate(String username, char[] password) { + if (CLibrary.libc.getpwnam(username) == null) { + logger.warn("Can not get PAM passwd for " + username); + return null; + } + + PAM pam = null; + try { + String serviceName = settings.getString(Keys.realm.pam.serviceName, "system-auth"); + pam = new PAM(serviceName); + pam.authenticate(username, new String(password)); + } catch (PAMException e) { + logger.error(e.getMessage()); + return null; + } finally { + pam.dispose(); + } + + UserModel user = userManager.getUserModel(username); + if (user == null) // create user object for new authenticated user + user = new UserModel(username.toLowerCase()); + + // create a user cookie + if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) { + user.cookie = StringUtils.getSHA1(user.username + new String(password)); + } + + // update user attributes from UnixUser + user.accountType = getAccountType(); + user.password = Constants.EXTERNAL_ACCOUNT; + + // TODO consider mapping PAM groups to teams + + // push the changes to the backing user service + updateUser(user); + + return user; + } +} diff --git a/src/main/java/com/gitblit/auth/RedmineAuthProvider.java b/src/main/java/com/gitblit/auth/RedmineAuthProvider.java new file mode 100644 index 00000000..176c576b --- /dev/null +++ b/src/main/java/com/gitblit/auth/RedmineAuthProvider.java @@ -0,0 +1,186 @@ +/* + * Copyright 2012 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.auth; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; + +import org.apache.wicket.util.io.IOUtils; + +import com.gitblit.Constants; +import com.gitblit.Constants.AccountType; +import com.gitblit.Keys; +import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider; +import com.gitblit.models.UserModel; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.ConnectionUtils; +import com.gitblit.utils.StringUtils; +import com.google.gson.Gson; + +/** + * Implementation of Redmine authentication.<br> + * you can login to gitblit with Redmine user id and api key. + */ +public class RedmineAuthProvider extends UsernamePasswordAuthenticationProvider { + + private String testingJson; + + private class RedmineCurrent { + private class RedmineUser { + public String login; + public String firstname; + public String lastname; + public String mail; + } + + public RedmineUser user; + } + + public RedmineAuthProvider() { + super("redmine"); + } + + @Override + public void setup() { + } + + @Override + public boolean supportsCredentialChanges() { + return false; + } + + @Override + public boolean supportsDisplayNameChanges() { + return false; + } + + @Override + public boolean supportsEmailAddressChanges() { + return false; + } + + @Override + public boolean supportsTeamMembershipChanges() { + return false; + } + + @Override + public AccountType getAccountType() { + return AccountType.REDMINE; + } + + @Override + public UserModel authenticate(String username, char[] password) { + String jsonString = null; + try { + // first attempt by username/password + jsonString = getCurrentUserAsJson(username, password); + } catch (Exception e1) { + logger.warn("Failed to authenticate via username/password against Redmine"); + try { + // second attempt is by apikey + jsonString = getCurrentUserAsJson(null, password); + username = null; + } catch (Exception e2) { + logger.error("Failed to authenticate via apikey against Redmine", e2); + return null; + } + } + + if (StringUtils.isEmpty(jsonString)) { + logger.error("Received empty authentication response from Redmine"); + return null; + } + + RedmineCurrent current = null; + try { + current = new Gson().fromJson(jsonString, RedmineCurrent.class); + } catch (Exception e) { + logger.error("Failed to deserialize Redmine json response: " + jsonString, e); + return null; + } + + if (StringUtils.isEmpty(username)) { + // if the username has been reset because of apikey authentication + // then use the email address of the user. this is the original + // behavior as contributed by github/mallowlabs + username = current.user.mail; + } + + UserModel user = userManager.getUserModel(username); + if (user == null) // create user object for new authenticated user + user = new UserModel(username.toLowerCase()); + + // create a user cookie + if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) { + user.cookie = StringUtils.getSHA1(user.username + new String(password)); + } + + // update user attributes from Redmine + user.accountType = getAccountType(); + user.displayName = current.user.firstname + " " + current.user.lastname; + user.emailAddress = current.user.mail; + user.password = Constants.EXTERNAL_ACCOUNT; + if (!StringUtils.isEmpty(current.user.login)) { + // only admin users can get login name + // evidently this is an undocumented behavior of Redmine + user.canAdmin = true; + } + + // TODO consider Redmine group mapping for team membership + // http://www.redmine.org/projects/redmine/wiki/Rest_Users + + // push the changes to the backing user service + updateUser(user); + + return user; + } + + private String getCurrentUserAsJson(String username, char [] password) throws IOException { + if (testingJson != null) { // for testing + return testingJson; + } + + String url = this.settings.getString(Keys.realm.redmine.url, ""); + if (!url.endsWith("/")) { + url = url.concat("/"); + } + HttpURLConnection http; + if (username == null) { + // apikey authentication + String apiKey = String.valueOf(password); + String apiUrl = url + "users/current.json?key=" + apiKey; + http = (HttpURLConnection) ConnectionUtils.openConnection(apiUrl, null, null); + } else { + // username/password BASIC authentication + String apiUrl = url + "users/current.json"; + http = (HttpURLConnection) ConnectionUtils.openConnection(apiUrl, username, password); + } + http.setRequestMethod("GET"); + http.connect(); + InputStreamReader reader = new InputStreamReader(http.getInputStream()); + return IOUtils.toString(reader); + } + + /** + * set json response. do NOT invoke from production code. + * @param json json + */ + public void setTestingCurrentUserAsJson(String json) { + this.testingJson = json; + } +} diff --git a/src/main/java/com/gitblit/auth/SalesforceAuthProvider.java b/src/main/java/com/gitblit/auth/SalesforceAuthProvider.java new file mode 100644 index 00000000..fdda32af --- /dev/null +++ b/src/main/java/com/gitblit/auth/SalesforceAuthProvider.java @@ -0,0 +1,128 @@ +package com.gitblit.auth; + +import com.gitblit.Constants; +import com.gitblit.Constants.AccountType; +import com.gitblit.Keys; +import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider; +import com.gitblit.models.UserModel; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.StringUtils; +import com.sforce.soap.partner.Connector; +import com.sforce.soap.partner.GetUserInfoResult; +import com.sforce.soap.partner.PartnerConnection; +import com.sforce.ws.ConnectionException; +import com.sforce.ws.ConnectorConfig; + +public class SalesforceAuthProvider extends UsernamePasswordAuthenticationProvider { + + public SalesforceAuthProvider() { + super("salesforce"); + } + + @Override + public AccountType getAccountType() { + return AccountType.SALESFORCE; + } + + @Override + public void setup() { + } + + @Override + public UserModel authenticate(String username, char[] password) { + ConnectorConfig config = new ConnectorConfig(); + config.setUsername(username); + config.setPassword(new String(password)); + + try { + PartnerConnection connection = Connector.newConnection(config); + + GetUserInfoResult info = connection.getUserInfo(); + + String org = settings.getString(Keys.realm.salesforce.orgId, "0") + .trim(); + + if (!org.equals("0")) { + if (!org.equals(info.getOrganizationId())) { + logger.warn("Access attempted by user of an invalid org: " + + info.getUserName() + ", org: " + + info.getOrganizationName() + "(" + + info.getOrganizationId() + ")"); + + return null; + } + } + + logger.info("Authenticated user " + info.getUserName() + + " using org " + info.getOrganizationName() + "(" + + info.getOrganizationId() + ")"); + + String simpleUsername = getSimpleUsername(info); + + UserModel user = null; + synchronized (this) { + user = userManager.getUserModel(simpleUsername); + if (user == null) + user = new UserModel(simpleUsername); + + if (StringUtils.isEmpty(user.cookie) + && !ArrayUtils.isEmpty(password)) { + user.cookie = StringUtils.getSHA1(user.username + + new String(password)); + } + + setUserAttributes(user, info); + + updateUser(user); + } + + return user; + } catch (ConnectionException e) { + logger.error("Failed to authenticate", e); + } + + return null; + } + + private void setUserAttributes(UserModel user, GetUserInfoResult info) { + // Don't want visibility into the real password, make up a dummy + user.password = Constants.EXTERNAL_ACCOUNT; + user.accountType = getAccountType(); + + // Get full name Attribute + user.displayName = info.getUserFullName(); + + // Get email address Attribute + user.emailAddress = info.getUserEmail(); + } + + /** + * Simple user name is the first part of the email address. + */ + private String getSimpleUsername(GetUserInfoResult info) { + String email = info.getUserEmail(); + + return email.split("@")[0]; + } + + + @Override + public boolean supportsCredentialChanges() { + return false; + } + + @Override + public boolean supportsDisplayNameChanges() { + return false; + } + + @Override + public boolean supportsEmailAddressChanges() { + return false; + } + + @Override + public boolean supportsTeamMembershipChanges() { + return true; + } +} diff --git a/src/main/java/com/gitblit/auth/WindowsAuthProvider.java b/src/main/java/com/gitblit/auth/WindowsAuthProvider.java new file mode 100644 index 00000000..d455d58f --- /dev/null +++ b/src/main/java/com/gitblit/auth/WindowsAuthProvider.java @@ -0,0 +1,177 @@ +/* + * Copyright 2013 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.auth; + +import java.util.Set; +import java.util.TreeSet; + +import waffle.windows.auth.IWindowsAccount; +import waffle.windows.auth.IWindowsAuthProvider; +import waffle.windows.auth.IWindowsComputer; +import waffle.windows.auth.IWindowsIdentity; +import waffle.windows.auth.impl.WindowsAuthProviderImpl; + +import com.gitblit.Constants; +import com.gitblit.Constants.AccountType; +import com.gitblit.Keys; +import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider; +import com.gitblit.models.UserModel; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.StringUtils; +import com.sun.jna.platform.win32.Win32Exception; + +/** + * Implementation of a Windows authentication provider. + * + * @author James Moger + */ +public class WindowsAuthProvider extends UsernamePasswordAuthenticationProvider { + + private IWindowsAuthProvider waffle; + + public WindowsAuthProvider() { + super("windows"); + } + + @Override + public void setup() { + + waffle = new WindowsAuthProviderImpl(); + IWindowsComputer computer = waffle.getCurrentComputer(); + logger.info("Windows Authentication Provider"); + logger.info(" name = " + computer.getComputerName()); + logger.info(" status = " + describeJoinStatus(computer.getJoinStatus())); + logger.info(" memberOf = " + computer.getMemberOf()); + //logger.info(" groups = " + Arrays.asList(computer.getGroups())); + } + + protected String describeJoinStatus(String value) { + if ("NetSetupUnknownStatus".equals(value)) { + return "unknown"; + } else if ("NetSetupUnjoined".equals(value)) { + return "not joined"; + } else if ("NetSetupWorkgroupName".equals(value)) { + return "joined to a workgroup"; + } else if ("NetSetupDomainName".equals(value)) { + return "joined to a domain"; + } + return value; + } + + @Override + public boolean supportsCredentialChanges() { + return false; + } + + @Override + public boolean supportsDisplayNameChanges() { + return false; + } + + @Override + public boolean supportsEmailAddressChanges() { + return true; + } + + @Override + public boolean supportsTeamMembershipChanges() { + return true; + } + + @Override + public AccountType getAccountType() { + return AccountType.WINDOWS; + } + + @Override + public UserModel authenticate(String username, char[] password) { + String defaultDomain = settings.getString(Keys.realm.windows.defaultDomain, null); + if (StringUtils.isEmpty(defaultDomain)) { + // ensure that default domain is null + defaultDomain = null; + } + + if (defaultDomain != null) { + // sanitize username + if (username.startsWith(defaultDomain + "\\")) { + // strip default domain from domain\ username + username = username.substring(defaultDomain.length() + 1); + } else if (username.endsWith("@" + defaultDomain)) { + // strip default domain from username@domain + username = username.substring(0, username.lastIndexOf('@')); + } + } + + IWindowsIdentity identity = null; + try { + if (username.indexOf('@') > -1 || username.indexOf('\\') > -1) { + // manually specified domain + identity = waffle.logonUser(username, new String(password)); + } else { + // no domain specified, use default domain + identity = waffle.logonDomainUser(username, defaultDomain, new String(password)); + } + } catch (Win32Exception e) { + logger.error(e.getMessage()); + return null; + } + + if (identity.isGuest() && !settings.getBoolean(Keys.realm.windows.allowGuests, false)) { + logger.warn("Guest account access is disabled"); + identity.dispose(); + return null; + } + + UserModel user = userManager.getUserModel(username); + if (user == null) // create user object for new authenticated user + user = new UserModel(username.toLowerCase()); + + // create a user cookie + if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) { + user.cookie = StringUtils.getSHA1(user.username + new String(password)); + } + + // update user attributes from Windows identity + user.accountType = getAccountType(); + String fqn = identity.getFqn(); + if (fqn.indexOf('\\') > -1) { + user.displayName = fqn.substring(fqn.lastIndexOf('\\') + 1); + } else { + user.displayName = fqn; + } + user.password = Constants.EXTERNAL_ACCOUNT; + + Set<String> groupNames = new TreeSet<String>(); + for (IWindowsAccount group : identity.getGroups()) { + groupNames.add(group.getFqn()); + } + + if (groupNames.contains("BUILTIN\\Administrators")) { + // local administrator + user.canAdmin = true; + } + + // TODO consider mapping Windows groups to teams + + // push the changes to the backing user service + updateUser(user); + + // cleanup resources + identity.dispose(); + + return user; + } +} |