summaryrefslogtreecommitdiffstats
path: root/src/main/java/com/gitblit/auth
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/gitblit/auth')
-rw-r--r--src/main/java/com/gitblit/auth/AuthenticationProvider.java182
-rw-r--r--src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java276
-rw-r--r--src/main/java/com/gitblit/auth/LdapAuthProvider.java508
-rw-r--r--src/main/java/com/gitblit/auth/PAMAuthProvider.java126
-rw-r--r--src/main/java/com/gitblit/auth/RedmineAuthProvider.java186
-rw-r--r--src/main/java/com/gitblit/auth/SalesforceAuthProvider.java128
-rw-r--r--src/main/java/com/gitblit/auth/WindowsAuthProvider.java177
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;
+ }
+}