summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMartin Spielmann <martin.spielmann@pingunaut.com>2016-11-18 22:39:20 +0100
committerGitHub <noreply@github.com>2016-11-18 22:39:20 +0100
commit88a91a87fad8158fc2c5ca2b9002b037894c6e97 (patch)
tree97d9639f68eab409f1db2dbb3cc47693218cb4a7 /src
parent77898a14f27c852664bcfb12f3c7322624381d6a (diff)
parentdfa3c3d3f944a91e0af7300df4012df6a5f5ea11 (diff)
downloadgitblit-88a91a87fad8158fc2c5ca2b9002b037894c6e97.tar.gz
gitblit-88a91a87fad8158fc2c5ca2b9002b037894c6e97.zip
Merge pull request #3 from gitblit/master
Get latest stuff from master before move forward with wicket-7
Diffstat (limited to 'src')
-rw-r--r--src/main/distrib/data/defaults.properties32
-rw-r--r--src/main/java/com/gitblit/Constants.java31
-rw-r--r--src/main/java/com/gitblit/auth/LdapAuthProvider.java398
-rw-r--r--src/main/java/com/gitblit/git/PatchsetReceivePack.java3
-rw-r--r--src/main/java/com/gitblit/manager/RepositoryManager.java71
-rw-r--r--src/main/java/com/gitblit/models/RepositoryModel.java3
-rw-r--r--src/main/java/com/gitblit/service/MailService.java17
-rw-r--r--src/main/java/com/gitblit/utils/ArrayUtils.java2
-rw-r--r--src/main/java/com/gitblit/utils/CommitCache.java117
-rw-r--r--src/main/java/com/gitblit/utils/JGitUtils.java343
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp.properties2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/BasePage.html1
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html3
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java6
-rw-r--r--src/main/java/com/gitblit/wicket/pages/TicketPage.java6
-rw-r--r--src/main/java/com/gitblit/wicket/panels/PagerPanel.java5
-rw-r--r--src/main/resources/bootstrap-fixes.css25
-rw-r--r--src/test/java/com/gitblit/tests/LdapAuthenticationTest.java352
18 files changed, 1122 insertions, 295 deletions
diff --git a/src/main/distrib/data/defaults.properties b/src/main/distrib/data/defaults.properties
index 0c7d6cd4..04166342 100644
--- a/src/main/distrib/data/defaults.properties
+++ b/src/main/distrib/data/defaults.properties
@@ -567,6 +567,21 @@ tickets.acceptNewPatchsets = true
# SINCE 1.4.0
tickets.requireApproval = false
+# Default setting to control how patchsets are merged to the integration branch.
+# Valid values:
+# MERGE_ALWAYS - Always merge with a merge commit. Every ticket will show up as a branch,
+# even if it could have been fast-forward merged. This is the default.
+# MERGE_IF_NECESSARY - If possible, fast-forward the integration branch,
+# if not, merge with a merge commit.
+# FAST_FORWARD_ONLY - Only merge when a fast-forward is possible. This produces a strictly
+# linear history of the integration branch.
+#
+# This setting can be overriden per-repository.
+#
+# RESTART REQUIRED
+# SINCE 1.9.0
+tickets.mergeType = MERGE_ALWAYS
+
# The case-insensitive regular expression used to identify and close tickets on
# push to the integration branch for commits that are NOT already referenced as
# a patchset tip.
@@ -1797,6 +1812,10 @@ realm.salesforce.orgId = 0
realm.ldap.server = ldap://localhost
# Login username for LDAP searches.
+# This is usually a user with permissions to search LDAP users and groups.
+# It must have at least have the permission to search users. If it does not
+# have permission to search groups, the normal user logging in must have
+# the permission in LDAP to search groups.
# If this value is unspecified, anonymous LDAP login will be used.
#
# e.g. mydomain\\username
@@ -1809,8 +1828,14 @@ realm.ldap.username = cn=Directory Manager
# SINCE 1.0.0
realm.ldap.password = password
-# Bind pattern for Authentication.
-# Allow to directly authenticate an user without LDAP Searches.
+# Bind pattern for user authentication.
+# Allow to directly authenticate an user without searching for it in LDAP.
+# Use this if the LDAP server does not allow anonymous access and you don't
+# want to use a specific account to run searches. When set, it will override
+# the settings realm.ldap.username and realm.ldap.password.
+# This requires that all relevant user entries are children to the same DN,
+# and that logging users have permission to search for their groups in LDAP.
+# This will disable synchronization as a specific LDAP account is needed for that.
#
# e.g. CN=${username},OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
#
@@ -1926,6 +1951,9 @@ realm.ldap.email = email
realm.ldap.uid = uid
# Defines whether to synchronize all LDAP users and teams into the user service
+# This requires either anonymous LDAP access or that a specific account is set
+# in realm.ldap.username and realm.ldap.password, that has permission to read
+# users and groups in LDAP.
#
# Valid values: true, false
# If left blank, false is assumed
diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java
index 9c5291ac..1af79f09 100644
--- a/src/main/java/com/gitblit/Constants.java
+++ b/src/main/java/com/gitblit/Constants.java
@@ -640,6 +640,37 @@ public class Constants {
}
}
+ /**
+ * The type of merge Gitblit will use when merging a ticket to the integration branch.
+ * <p>
+ * The default type is MERGE_ALWAYS.
+ * <p>
+ * This is modeled after the Gerrit SubmitType.
+ */
+ public static enum MergeType {
+ /** Allows a merge only if it can be fast-forward merged into the integration branch. */
+ FAST_FORWARD_ONLY,
+ /** Uses a fast-forward merge if possible, other wise a merge commit is created. */
+ MERGE_IF_NECESSARY,
+ // Future REBASE_IF_NECESSARY,
+ /** Always merge with a merge commit, even when a fast-forward would be possible. */
+ MERGE_ALWAYS,
+ // Future? CHERRY_PICK
+ ;
+
+ public static final MergeType DEFAULT_MERGE_TYPE = MERGE_ALWAYS;
+
+ public static MergeType fromName(String name) {
+ for (MergeType type : values()) {
+ if (type.name().equalsIgnoreCase(name)) {
+ return type;
+ }
+ }
+ return DEFAULT_MERGE_TYPE;
+ }
+ }
+
+
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Unused {
diff --git a/src/main/java/com/gitblit/auth/LdapAuthProvider.java b/src/main/java/com/gitblit/auth/LdapAuthProvider.java
index cc772e7b..e1dec48f 100644
--- a/src/main/java/com/gitblit/auth/LdapAuthProvider.java
+++ b/src/main/java/com/gitblit/auth/LdapAuthProvider.java
@@ -39,6 +39,8 @@ import com.gitblit.service.LdapSyncService;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.StringUtils;
import com.unboundid.ldap.sdk.Attribute;
+import com.unboundid.ldap.sdk.BindRequest;
+import com.unboundid.ldap.sdk.BindResult;
import com.unboundid.ldap.sdk.DereferencePolicy;
import com.unboundid.ldap.sdk.ExtendedResult;
import com.unboundid.ldap.sdk.LDAPConnection;
@@ -107,8 +109,14 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
if (enabled) {
logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server));
final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.removeDeletedUsers, true);
- LDAPConnection ldapConnection = getLdapConnection();
- if (ldapConnection != null) {
+ LdapConnection ldapConnection = new LdapConnection();
+ if (ldapConnection.connect()) {
+ if (ldapConnection.bind() == null) {
+ ldapConnection.close();
+ logger.error("Cannot synchronize with LDAP.");
+ return;
+ }
+
try {
String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid");
@@ -179,66 +187,6 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
}
}
- 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());
- if (ldapPort == -1) {
- ldapPort = 636;
- }
- } else if (ldapUrl.getScheme().equalsIgnoreCase("ldap") || ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
- // no encryption or StartTLS
- conn = new LDAPConnection();
- if (ldapPort == -1) {
- ldapPort = 389;
- }
- } 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)) {
- // anonymous bind
- conn.bind(new SimpleBindRequest());
- } else {
- // authenticated bind
- 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.
@@ -321,23 +269,26 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
public UserModel authenticate(String username, char[] password) {
String simpleUsername = getSimpleUsername(username);
- LDAPConnection ldapConnection = getLdapConnection();
- if (ldapConnection != null) {
- try {
- boolean alreadyAuthenticated = false;
+ LdapConnection ldapConnection = new LdapConnection();
+ if (ldapConnection.connect()) {
- String bindPattern = settings.getString(Keys.realm.ldap.bindpattern, "");
- if (!StringUtils.isEmpty(bindPattern)) {
- try {
- String bindUser = StringUtils.replace(bindPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
- ldapConnection.bind(bindUser, new String(password));
+ // Try to bind either to the "manager" account,
+ // or directly to the DN of the user logging in, if realm.ldap.bindpattern is configured.
+ String passwd = new String(password);
+ BindResult bindResult = null;
+ String bindPattern = settings.getString(Keys.realm.ldap.bindpattern, "");
+ if (! StringUtils.isEmpty(bindPattern)) {
+ bindResult = ldapConnection.bind(bindPattern, simpleUsername, passwd);
+ } else {
+ bindResult = ldapConnection.bind();
+ }
+ if (bindResult == null) {
+ ldapConnection.close();
+ return null;
+ }
- alreadyAuthenticated = true;
- } catch (LDAPException e) {
- return 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}))");
@@ -348,7 +299,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
SearchResultEntry loggingInUser = result.getSearchEntries().get(0);
String loggingInUserDN = loggingInUser.getDN();
- if (alreadyAuthenticated || isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) {
+ if (ldapConnection.isAuthenticated(loggingInUserDN, passwd)) {
logger.debug("LDAP authenticated: " + username);
UserModel user = null;
@@ -462,7 +413,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
}
}
- private void getTeamsFromLdap(LDAPConnection ldapConnection, String simpleUsername, SearchResultEntry loggingInUser, UserModel user) {
+ private void getTeamsFromLdap(LdapConnection ldapConnection, String simpleUsername, SearchResultEntry loggingInUser, UserModel user) {
String loggingInUserDN = loggingInUser.getDN();
// Clear the users team memberships - we're going to get them from LDAP
@@ -479,7 +430,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue()));
}
- SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, true, groupMemberPattern, Arrays.asList("cn"));
+ SearchResult teamMembershipResult = searchTeamsInLdap(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);
@@ -496,12 +447,12 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
}
}
- private void getEmptyTeamsFromLdap(LDAPConnection ldapConnection) {
+ private void getEmptyTeamsFromLdap(LdapConnection ldapConnection) {
logger.info("Start fetching empty teams from ldap.");
String groupBase = settings.getString(Keys.realm.ldap.groupBase, "");
String groupMemberPattern = settings.getString(Keys.realm.ldap.groupEmptyMemberPattern, "(&(objectClass=group)(!(member=*)))");
- SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, true, groupMemberPattern, null);
+ SearchResult teamMembershipResult = searchTeamsInLdap(ldapConnection, groupBase, true, groupMemberPattern, null);
if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) {
for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) {
SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i);
@@ -519,6 +470,30 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
logger.info("Finished fetching empty teams from ldap.");
}
+
+ private SearchResult searchTeamsInLdap(LdapConnection ldapConnection, String base, boolean dereferenceAliases, String filter, List<String> attributes) {
+ SearchResult result = ldapConnection.search(base, dereferenceAliases, filter, attributes);
+ if (result == null) {
+ return null;
+ }
+
+ if (result.getResultCode() != ResultCode.SUCCESS) {
+ // Retry the search with user authorization in case we searched as a manager account that could not search for teams.
+ logger.debug("Rebinding as user to search for teams in LDAP");
+ result = null;
+ if (ldapConnection.rebindAsUser()) {
+ result = ldapConnection.search(base, dereferenceAliases, filter, attributes);
+ if (result.getResultCode() != ResultCode.SUCCESS) {
+ return null;
+ }
+ logger.info("Successful search after rebinding as user.");
+ }
+ }
+
+ return result;
+ }
+
+
private TeamModel createTeamFromLdap(SearchResultEntry teamEntry) {
TeamModel answer = new TeamModel(teamEntry.getAttributeValue("cn"));
answer.accountType = getAccountType();
@@ -527,47 +502,21 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
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) {
+ private SearchResult doSearch(LdapConnection ldapConnection, String base, String filter) {
try {
SearchRequest searchRequest = new SearchRequest(base, SearchScope.SUB, filter);
- if (dereferenceAliases) {
- searchRequest.setDerefPolicy(DereferencePolicy.SEARCHING);
- }
- if (attributes != null) {
- searchRequest.setAttributes(attributes);
+ SearchResult result = ldapConnection.search(searchRequest);
+ if (result.getResultCode() != ResultCode.SUCCESS) {
+ return null;
}
- return ldapConnection.search(searchRequest);
-
- } catch (LDAPSearchException e) {
- logger.error("Problem Searching LDAP", e);
-
- return null;
+ return result;
} 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.
@@ -585,7 +534,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
}
// From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
- public static final String escapeLDAPSearchFilter(String filter) {
+ private static final String escapeLDAPSearchFilter(String filter) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < filter.length(); i++) {
char curChar = filter.charAt(i);
@@ -625,4 +574,225 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
}
}
+
+
+ private class LdapConnection {
+ private LDAPConnection conn;
+ private SimpleBindRequest currentBindRequest;
+ private SimpleBindRequest managerBindRequest;
+ private SimpleBindRequest userBindRequest;
+
+
+ public LdapConnection() {
+ String bindUserName = settings.getString(Keys.realm.ldap.username, "");
+ String bindPassword = settings.getString(Keys.realm.ldap.password, "");
+ if (StringUtils.isEmpty(bindUserName) && StringUtils.isEmpty(bindPassword)) {
+ this.managerBindRequest = new SimpleBindRequest();
+ }
+ this.managerBindRequest = new SimpleBindRequest(bindUserName, bindPassword);
+ }
+
+
+ boolean connect() {
+ try {
+ URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));
+ String ldapHost = ldapUrl.getHost();
+ int ldapPort = ldapUrl.getPort();
+
+ if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) {
+ // SSL
+ SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
+ conn = new LDAPConnection(sslUtil.createSSLSocketFactory());
+ if (ldapPort == -1) {
+ ldapPort = 636;
+ }
+ } else if (ldapUrl.getScheme().equalsIgnoreCase("ldap") || ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
+ // no encryption or StartTLS
+ conn = new LDAPConnection();
+ if (ldapPort == -1) {
+ ldapPort = 389;
+ }
+ } else {
+ logger.error("Unsupported LDAP URL scheme: " + ldapUrl.getScheme());
+ return false;
+ }
+
+ 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());
+ }
+ }
+
+ return true;
+
+ } 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 false;
+ }
+
+
+ void close() {
+ if (conn != null) {
+ conn.close();
+ }
+ }
+
+
+ SearchResult search(SearchRequest request) {
+ try {
+ return conn.search(request);
+ } catch (LDAPSearchException e) {
+ logger.error("Problem Searching LDAP [{}]", e.getResultCode());
+ return e.getSearchResult();
+ }
+ }
+
+
+ SearchResult search(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);
+ }
+ SearchResult result = search(searchRequest);
+ return result;
+
+ } catch (LDAPException e) {
+ logger.error("Problem creating LDAP search", e);
+ return null;
+ }
+ }
+
+
+
+ /**
+ * Bind using the manager credentials set in realm.ldap.username and ..password
+ * @return A bind result, or null if binding failed.
+ */
+ BindResult bind() {
+ BindResult result = null;
+ try {
+ result = conn.bind(managerBindRequest);
+ currentBindRequest = managerBindRequest;
+ } catch (LDAPException e) {
+ logger.error("Error authenticating to LDAP with manager account to search the directory.");
+ logger.error(" Please check your settings for realm.ldap.username and realm.ldap.password.");
+ logger.debug(" Received exception when binding to LDAP", e);
+ return null;
+ }
+ return result;
+ }
+
+
+ /**
+ * Bind using the given credentials, by filling in the username in the given {@code bindPattern} to
+ * create the DN.
+ * @return A bind result, or null if binding failed.
+ */
+ BindResult bind(String bindPattern, String simpleUsername, String password) {
+ BindResult result = null;
+ try {
+ String bindUser = StringUtils.replace(bindPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
+ SimpleBindRequest request = new SimpleBindRequest(bindUser, password);
+ result = conn.bind(request);
+ userBindRequest = request;
+ currentBindRequest = userBindRequest;
+ } catch (LDAPException e) {
+ logger.error("Error authenticating to LDAP with user account to search the directory.");
+ logger.error(" Please check your settings for realm.ldap.bindpattern.");
+ logger.debug(" Received exception when binding to LDAP", e);
+ return null;
+ }
+ return result;
+ }
+
+
+ boolean rebindAsUser() {
+ if (userBindRequest == null || currentBindRequest == userBindRequest) {
+ return false;
+ }
+ try {
+ conn.bind(userBindRequest);
+ currentBindRequest = userBindRequest;
+ } catch (LDAPException e) {
+ conn.close();
+ logger.error("Error rebinding to LDAP with user account.", e);
+ return false;
+ }
+ return true;
+ }
+
+
+ boolean isAuthenticated(String userDn, String password) {
+ verifyCurrentBinding();
+
+ // If the currently bound DN is already the DN of the logging in user, authentication has already happened
+ // during the previous bind operation. We accept this and return with the current bind left in place.
+ // This could also be changed to always retry binding as the logging in user, to make sure that the
+ // connection binding has not been tampered with in between. So far I see no way how this could happen
+ // and thus skip the repeated binding.
+ // This check also makes sure that the DN in realm.ldap.bindpattern actually matches the DN that was found
+ // when searching the user entry.
+ String boundDN = currentBindRequest.getBindDN();
+ if (boundDN != null && boundDN.equals(userDn)) {
+ return true;
+ }
+
+ // Bind a the logging in user to check for authentication.
+ // Afterwards, bind as the original bound DN again, to restore the previous authorization.
+ boolean isAuthenticated = false;
+ try {
+ // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN
+ SimpleBindRequest ubr = new SimpleBindRequest(userDn, password);
+ conn.bind(ubr);
+ isAuthenticated = true;
+ userBindRequest = ubr;
+ } catch (LDAPException e) {
+ logger.error("Error authenticating user ({})", userDn, e);
+ }
+
+ try {
+ conn.bind(currentBindRequest);
+ } catch (LDAPException e) {
+ logger.error("Error reinstating original LDAP authorization (code {}). Team information may be inaccurate for this log in.",
+ e.getResultCode(), e);
+ }
+ return isAuthenticated;
+ }
+
+
+
+ private boolean verifyCurrentBinding() {
+ BindRequest lastBind = conn.getLastBindRequest();
+ if (lastBind == currentBindRequest) {
+ return true;
+ }
+ logger.debug("Unexpected binding in LdapConnection. {} != {}", lastBind, currentBindRequest);
+
+ String lastBoundDN = ((SimpleBindRequest)lastBind).getBindDN();
+ String boundDN = currentBindRequest.getBindDN();
+ logger.debug("Currently bound as '{}', check authentication for '{}'", lastBoundDN, boundDN);
+ if (boundDN != null && ! boundDN.equals(lastBoundDN)) {
+ logger.warn("Unexpected binding DN in LdapConnection. '{}' != '{}'.", lastBoundDN, boundDN);
+ logger.warn("Updated binding information in LDAP connection.");
+ currentBindRequest = (SimpleBindRequest)lastBind;
+ return false;
+ }
+ return true;
+ }
+ }
}
diff --git a/src/main/java/com/gitblit/git/PatchsetReceivePack.java b/src/main/java/com/gitblit/git/PatchsetReceivePack.java
index 33fa4705..4a09139a 100644
--- a/src/main/java/com/gitblit/git/PatchsetReceivePack.java
+++ b/src/main/java/com/gitblit/git/PatchsetReceivePack.java
@@ -599,7 +599,7 @@ public class PatchsetReceivePack extends GitblitReceivePack {
}
// ensure that the patchset can be cleanly merged right now
- MergeStatus status = JGitUtils.canMerge(getRepository(), tipCommit.getName(), forBranch);
+ MergeStatus status = JGitUtils.canMerge(getRepository(), tipCommit.getName(), forBranch, repository.mergeType);
switch (status) {
case ALREADY_MERGED:
sendError("");
@@ -1279,6 +1279,7 @@ public class PatchsetReceivePack extends GitblitReceivePack {
getRepository(),
patchset.tip,
ticket.mergeTo,
+ getRepositoryModel().mergeType,
committer,
message);
diff --git a/src/main/java/com/gitblit/manager/RepositoryManager.java b/src/main/java/com/gitblit/manager/RepositoryManager.java
index e9bf5b84..2be65873 100644
--- a/src/main/java/com/gitblit/manager/RepositoryManager.java
+++ b/src/main/java/com/gitblit/manager/RepositoryManager.java
@@ -63,6 +63,7 @@ import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.Constants.CommitMessageRenderer;
import com.gitblit.Constants.FederationStrategy;
+import com.gitblit.Constants.MergeType;
import com.gitblit.Constants.PermissionType;
import com.gitblit.Constants.RegistrantType;
import com.gitblit.GitBlitException;
@@ -899,6 +900,7 @@ public class RepositoryManager implements IRepositoryManager {
model.acceptNewTickets = getConfig(config, "acceptNewTickets", true);
model.requireApproval = getConfig(config, "requireApproval", settings.getBoolean(Keys.tickets.requireApproval, false));
model.mergeTo = getConfig(config, "mergeTo", null);
+ model.mergeType = MergeType.fromName(getConfig(config, "mergeType", settings.getString(Keys.tickets.mergeType, null)));
model.useIncrementalPushTags = getConfig(config, "useIncrementalPushTags", false);
model.incrementalPushTagPrefix = getConfig(config, "incrementalPushTagPrefix", null);
model.allowForks = getConfig(config, "allowForks", true);
@@ -1557,6 +1559,13 @@ public class RepositoryManager implements IRepositoryManager {
if (!StringUtils.isEmpty(repository.mergeTo)) {
config.setString(Constants.CONFIG_GITBLIT, null, "mergeTo", repository.mergeTo);
}
+ if (repository.mergeType == null || repository.mergeType == MergeType.fromName(settings.getString(Keys.tickets.mergeType, null))) {
+ // use default
+ config.unset(Constants.CONFIG_GITBLIT, null, "mergeType");
+ } else {
+ // override default
+ config.setString(Constants.CONFIG_GITBLIT, null, "mergeType", repository.mergeType.name());
+ }
config.setBoolean(Constants.CONFIG_GITBLIT, null, "useIncrementalPushTags", repository.useIncrementalPushTags);
if (StringUtils.isEmpty(repository.incrementalPushTagPrefix) ||
repository.incrementalPushTagPrefix.equals(settings.getString(Keys.git.defaultIncrementalPushTagPrefix, "r"))) {
@@ -1952,39 +1961,47 @@ public class RepositoryManager implements IRepositoryManager {
}
protected void configureCommitCache() {
- int daysToCache = settings.getInteger(Keys.web.activityCacheDays, 14);
+ final int daysToCache = settings.getInteger(Keys.web.activityCacheDays, 14);
if (daysToCache <= 0) {
logger.info("Commit cache is disabled");
- } else {
- long start = System.nanoTime();
- long repoCount = 0;
- long commitCount = 0;
- logger.info(MessageFormat.format("Preparing {0} day commit cache. please wait...", daysToCache));
- CommitCache.instance().setCacheDays(daysToCache);
- Date cutoff = CommitCache.instance().getCutoffDate();
- for (String repositoryName : getRepositoryList()) {
- RepositoryModel model = getRepositoryModel(repositoryName);
- if (model != null && model.hasCommits && model.lastChange.after(cutoff)) {
- repoCount++;
- Repository repository = getRepository(repositoryName);
- for (RefModel ref : JGitUtils.getLocalBranches(repository, true, -1)) {
- if (!ref.getDate().after(cutoff)) {
- // branch not recently updated
- continue;
- }
- List<?> commits = CommitCache.instance().getCommits(repositoryName, repository, ref.getName());
- if (commits.size() > 0) {
- logger.info(MessageFormat.format(" cached {0} commits for {1}:{2}",
- commits.size(), repositoryName, ref.getName()));
- commitCount += commits.size();
+ return;
+ }
+ logger.info(MessageFormat.format("Preparing {0} day commit cache...", daysToCache));
+ CommitCache.instance().setCacheDays(daysToCache);
+ Thread loader = new Thread() {
+ @Override
+ public void run() {
+ long start = System.nanoTime();
+ long repoCount = 0;
+ long commitCount = 0;
+ Date cutoff = CommitCache.instance().getCutoffDate();
+ for (String repositoryName : getRepositoryList()) {
+ RepositoryModel model = getRepositoryModel(repositoryName);
+ if (model != null && model.hasCommits && model.lastChange.after(cutoff)) {
+ repoCount++;
+ Repository repository = getRepository(repositoryName);
+ for (RefModel ref : JGitUtils.getLocalBranches(repository, true, -1)) {
+ if (!ref.getDate().after(cutoff)) {
+ // branch not recently updated
+ continue;
+ }
+ List<?> commits = CommitCache.instance().getCommits(repositoryName, repository, ref.getName());
+ if (commits.size() > 0) {
+ logger.info(MessageFormat.format(" cached {0} commits for {1}:{2}",
+ commits.size(), repositoryName, ref.getName()));
+ commitCount += commits.size();
+ }
}
+ repository.close();
}
- repository.close();
}
+ logger.info(MessageFormat.format("built {0} day commit cache of {1} commits across {2} repositories in {3} msecs",
+ daysToCache, commitCount, repoCount, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
}
- logger.info(MessageFormat.format("built {0} day commit cache of {1} commits across {2} repositories in {3} msecs",
- daysToCache, commitCount, repoCount, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
- }
+ };
+ loader.setName("CommitCacheLoader");
+ loader.setDaemon(true);
+ loader.start();
}
protected void confirmWriteAccess() {
diff --git a/src/main/java/com/gitblit/models/RepositoryModel.java b/src/main/java/com/gitblit/models/RepositoryModel.java
index a81c622a..67ee1c7e 100644
--- a/src/main/java/com/gitblit/models/RepositoryModel.java
+++ b/src/main/java/com/gitblit/models/RepositoryModel.java
@@ -28,6 +28,7 @@ import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.Constants.CommitMessageRenderer;
import com.gitblit.Constants.FederationStrategy;
+import com.gitblit.Constants.MergeType;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.ModelUtils;
import com.gitblit.utils.StringUtils;
@@ -89,6 +90,7 @@ public class RepositoryModel implements Serializable, Comparable<RepositoryModel
public boolean acceptNewTickets;
public boolean requireApproval;
public String mergeTo;
+ public MergeType mergeType;
public transient boolean isCollectingGarbage;
public Date lastGC;
@@ -111,6 +113,7 @@ public class RepositoryModel implements Serializable, Comparable<RepositoryModel
this.isBare = true;
this.acceptNewTickets = true;
this.acceptNewPatchsets = true;
+ this.mergeType = MergeType.DEFAULT_MERGE_TYPE;
addOwner(owner);
}
diff --git a/src/main/java/com/gitblit/service/MailService.java b/src/main/java/com/gitblit/service/MailService.java
index ec3a84ca..58acc9c0 100644
--- a/src/main/java/com/gitblit/service/MailService.java
+++ b/src/main/java/com/gitblit/service/MailService.java
@@ -17,6 +17,7 @@ package com.gitblit.service;
import java.io.File;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Properties;
@@ -31,6 +32,7 @@ import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
+import javax.mail.SendFailedException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
@@ -272,9 +274,22 @@ public class MailService implements Runnable {
while ((message = queue.poll()) != null) {
try {
if (settings.getBoolean(Keys.mail.debug, false)) {
- logger.info("send: " + StringUtils.trimString(message.getSubject(), 60));
+ logger.info("send: '" + StringUtils.trimString(message.getSubject(), 60)
+ + "' to:" + StringUtils.trimString(Arrays.toString(message.getAllRecipients()), 300));
}
Transport.send(message);
+ } catch (SendFailedException sfe) {
+ if (settings.getBoolean(Keys.mail.debug, false)) {
+ logger.error("Failed to send message: {}", sfe.getMessage());
+ logger.info(" Invalid addresses: {}", Arrays.toString(sfe.getInvalidAddresses()));
+ logger.info(" Valid sent addresses: {}", Arrays.toString(sfe.getValidSentAddresses()));
+ logger.info(" Valid unset addresses: {}", Arrays.toString(sfe.getValidUnsentAddresses()));
+ logger.info("", sfe);
+ }
+ else {
+ logger.error("Failed to send message: {}", sfe.getMessage(), sfe.getNextException());
+ }
+ failures.add(message);
} catch (Throwable e) {
logger.error("Failed to send message", e);
failures.add(message);
diff --git a/src/main/java/com/gitblit/utils/ArrayUtils.java b/src/main/java/com/gitblit/utils/ArrayUtils.java
index 1402ad5e..b850ccc9 100644
--- a/src/main/java/com/gitblit/utils/ArrayUtils.java
+++ b/src/main/java/com/gitblit/utils/ArrayUtils.java
@@ -42,7 +42,7 @@ public class ArrayUtils {
}
public static boolean isEmpty(Collection<?> collection) {
- return collection == null || collection.size() == 0;
+ return collection == null || collection.isEmpty();
}
public static String toString(Collection<?> collection) {
diff --git a/src/main/java/com/gitblit/utils/CommitCache.java b/src/main/java/com/gitblit/utils/CommitCache.java
index a3963f50..53b8de19 100644
--- a/src/main/java/com/gitblit/utils/CommitCache.java
+++ b/src/main/java/com/gitblit/utils/CommitCache.java
@@ -19,9 +19,9 @@ import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.ObjectId;
@@ -58,7 +58,7 @@ public class CommitCache {
}
protected CommitCache() {
- cache = new ConcurrentHashMap<String, ObjectCache<List<RepositoryCommit>>>();
+ cache = new HashMap<>();
}
/**
@@ -93,7 +93,9 @@ public class CommitCache {
*
*/
public void clear() {
- cache.clear();
+ synchronized (cache) {
+ cache.clear();
+ }
}
/**
@@ -103,8 +105,11 @@ public class CommitCache {
*/
public void clear(String repositoryName) {
String repoKey = repositoryName.toLowerCase();
- ObjectCache<List<RepositoryCommit>> repoCache = cache.remove(repoKey);
- if (repoCache != null) {
+ boolean hadEntries = false;
+ synchronized (cache) {
+ hadEntries = cache.remove(repoKey) != null;
+ }
+ if (hadEntries) {
logger.info(MessageFormat.format("{0} commit cache cleared", repositoryName));
}
}
@@ -117,13 +122,17 @@ public class CommitCache {
*/
public void clear(String repositoryName, String branch) {
String repoKey = repositoryName.toLowerCase();
- ObjectCache<List<RepositoryCommit>> repoCache = cache.get(repoKey);
- if (repoCache != null) {
- List<RepositoryCommit> commits = repoCache.remove(branch.toLowerCase());
- if (!ArrayUtils.isEmpty(commits)) {
- logger.info(MessageFormat.format("{0}:{1} commit cache cleared", repositoryName, branch));
+ boolean hadEntries = false;
+ synchronized (cache) {
+ ObjectCache<List<RepositoryCommit>> repoCache = cache.get(repoKey);
+ if (repoCache != null) {
+ List<RepositoryCommit> commits = repoCache.remove(branch.toLowerCase());
+ hadEntries = !ArrayUtils.isEmpty(commits);
}
}
+ if (hadEntries) {
+ logger.info(MessageFormat.format("{0}:{1} commit cache cleared", repositoryName, branch));
+ }
}
/**
@@ -156,49 +165,55 @@ public class CommitCache {
if (cacheDays > 0 && (sinceDate.getTime() >= cacheCutoffDate.getTime())) {
// request fits within the cache window
String repoKey = repositoryName.toLowerCase();
- if (!cache.containsKey(repoKey)) {
- cache.put(repoKey, new ObjectCache<List<RepositoryCommit>>());
- }
-
- ObjectCache<List<RepositoryCommit>> repoCache = cache.get(repoKey);
String branchKey = branch.toLowerCase();
RevCommit tip = JGitUtils.getCommit(repository, branch);
Date tipDate = JGitUtils.getCommitDate(tip);
- List<RepositoryCommit> commits;
- if (!repoCache.hasCurrent(branchKey, tipDate)) {
- commits = repoCache.getObject(branchKey);
- if (ArrayUtils.isEmpty(commits)) {
- // we don't have any cached commits for this branch, reload
- commits = get(repositoryName, repository, branch, cacheCutoffDate);
- repoCache.updateObject(branchKey, tipDate, commits);
- logger.debug(MessageFormat.format("parsed {0} commits from {1}:{2} since {3,date,yyyy-MM-dd} in {4} msecs",
- commits.size(), repositoryName, branch, cacheCutoffDate, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
- } else {
- // incrementally update cache since the last cached commit
- ObjectId sinceCommit = commits.get(0).getId();
- List<RepositoryCommit> incremental = get(repositoryName, repository, branch, sinceCommit);
- logger.info(MessageFormat.format("incrementally added {0} commits to cache for {1}:{2} in {3} msecs",
- incremental.size(), repositoryName, branch, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
- incremental.addAll(commits);
- repoCache.updateObject(branchKey, tipDate, incremental);
- commits = incremental;
+ ObjectCache<List<RepositoryCommit>> repoCache;
+ synchronized (cache) {
+ repoCache = cache.get(repoKey);
+ if (repoCache == null) {
+ repoCache = new ObjectCache<>();
+ cache.put(repoKey, repoCache);
}
- } else {
- // cache is current
- commits = repoCache.getObject(branchKey);
- // evict older commits outside the cache window
- commits = reduce(commits, cacheCutoffDate);
- // update cache
- repoCache.updateObject(branchKey, tipDate, commits);
}
+ synchronized (repoCache) {
+ List<RepositoryCommit> commits;
+ if (!repoCache.hasCurrent(branchKey, tipDate)) {
+ commits = repoCache.getObject(branchKey);
+ if (ArrayUtils.isEmpty(commits)) {
+ // we don't have any cached commits for this branch, reload
+ commits = get(repositoryName, repository, branch, cacheCutoffDate);
+ repoCache.updateObject(branchKey, tipDate, commits);
+ logger.debug(MessageFormat.format("parsed {0} commits from {1}:{2} since {3,date,yyyy-MM-dd} in {4} msecs",
+ commits.size(), repositoryName, branch, cacheCutoffDate, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
+ } else {
+ // incrementally update cache since the last cached commit
+ ObjectId sinceCommit = commits.get(0).getId();
+ List<RepositoryCommit> incremental = get(repositoryName, repository, branch, sinceCommit);
+ logger.info(MessageFormat.format("incrementally added {0} commits to cache for {1}:{2} in {3} msecs",
+ incremental.size(), repositoryName, branch, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
+ incremental.addAll(commits);
+ repoCache.updateObject(branchKey, tipDate, incremental);
+ commits = incremental;
+ }
+ } else {
+ // cache is current
+ commits = repoCache.getObject(branchKey);
+ // evict older commits outside the cache window
+ commits = reduce(commits, cacheCutoffDate);
+ // update cache
+ repoCache.updateObject(branchKey, tipDate, commits);
+ }
- if (sinceDate.equals(cacheCutoffDate)) {
- list = commits;
- } else {
- // reduce the commits to those since the specified date
- list = reduce(commits, sinceDate);
+ if (sinceDate.equals(cacheCutoffDate)) {
+ // Mustn't hand out the cached list; that's not thread-safe
+ list = new ArrayList<>(commits);
+ } else {
+ // reduce the commits to those since the specified date
+ list = reduce(commits, sinceDate);
+ }
}
logger.debug(MessageFormat.format("retrieved {0} commits from cache of {1}:{2} since {3,date,yyyy-MM-dd} in {4} msecs",
list.size(), repositoryName, branch, sinceDate, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
@@ -222,8 +237,9 @@ public class CommitCache {
*/
protected List<RepositoryCommit> get(String repositoryName, Repository repository, String branch, Date sinceDate) {
Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository, false);
- List<RepositoryCommit> commits = new ArrayList<RepositoryCommit>();
- for (RevCommit commit : JGitUtils.getRevLog(repository, branch, sinceDate)) {
+ List<RevCommit> revLog = JGitUtils.getRevLog(repository, branch, sinceDate);
+ List<RepositoryCommit> commits = new ArrayList<RepositoryCommit>(revLog.size());
+ for (RevCommit commit : revLog) {
RepositoryCommit commitModel = new RepositoryCommit(repositoryName, branch, commit);
List<RefModel> commitRefs = allRefs.get(commitModel.getId());
commitModel.setRefs(commitRefs);
@@ -243,8 +259,9 @@ public class CommitCache {
*/
protected List<RepositoryCommit> get(String repositoryName, Repository repository, String branch, ObjectId sinceCommit) {
Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository, false);
- List<RepositoryCommit> commits = new ArrayList<RepositoryCommit>();
- for (RevCommit commit : JGitUtils.getRevLog(repository, sinceCommit.getName(), branch)) {
+ List<RevCommit> revLog = JGitUtils.getRevLog(repository, sinceCommit.getName(), branch);
+ List<RepositoryCommit> commits = new ArrayList<RepositoryCommit>(revLog.size());
+ for (RevCommit commit : revLog) {
RepositoryCommit commitModel = new RepositoryCommit(repositoryName, branch, commit);
List<RefModel> commitRefs = allRefs.get(commitModel.getId());
commitModel.setRefs(commitRefs);
@@ -261,7 +278,7 @@ public class CommitCache {
* @return a list of commits
*/
protected List<RepositoryCommit> reduce(List<RepositoryCommit> commits, Date sinceDate) {
- List<RepositoryCommit> filtered = new ArrayList<RepositoryCommit>();
+ List<RepositoryCommit> filtered = new ArrayList<RepositoryCommit>(commits.size());
for (RepositoryCommit commit : commits) {
if (commit.getCommitDate().compareTo(sinceDate) >= 0) {
filtered.add(commit);
diff --git a/src/main/java/com/gitblit/utils/JGitUtils.java b/src/main/java/com/gitblit/utils/JGitUtils.java
index a02fc3ff..0eea1d61 100644
--- a/src/main/java/com/gitblit/utils/JGitUtils.java
+++ b/src/main/java/com/gitblit/utils/JGitUtils.java
@@ -99,6 +99,7 @@ import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.gitblit.Constants.MergeType;
import com.gitblit.GitBlitException;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
@@ -2453,44 +2454,13 @@ public class JGitUtils {
* @param repository
* @param src
* @param toBranch
+ * @param mergeType
+ * Defines the integration strategy to use for merging.
* @return true if we can merge without conflict
*/
- public static MergeStatus canMerge(Repository repository, String src, String toBranch) {
- RevWalk revWalk = null;
- try {
- revWalk = new RevWalk(repository);
- ObjectId branchId = repository.resolve(toBranch);
- if (branchId == null) {
- return MergeStatus.MISSING_INTEGRATION_BRANCH;
- }
- ObjectId srcId = repository.resolve(src);
- if (srcId == null) {
- return MergeStatus.MISSING_SRC_BRANCH;
- }
- RevCommit branchTip = revWalk.lookupCommit(branchId);
- RevCommit srcTip = revWalk.lookupCommit(srcId);
- if (revWalk.isMergedInto(srcTip, branchTip)) {
- // already merged
- return MergeStatus.ALREADY_MERGED;
- } else if (revWalk.isMergedInto(branchTip, srcTip)) {
- // fast-forward
- return MergeStatus.MERGEABLE;
- }
- RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
- boolean canMerge = merger.merge(branchTip, srcTip);
- if (canMerge) {
- return MergeStatus.MERGEABLE;
- }
- } catch (NullPointerException e) {
- LOGGER.error("Failed to determine canMerge", e);
- } catch (IOException e) {
- LOGGER.error("Failed to determine canMerge", e);
- } finally {
- if (revWalk != null) {
- revWalk.close();
- }
- }
- return MergeStatus.NOT_MERGEABLE;
+ public static MergeStatus canMerge(Repository repository, String src, String toBranch, MergeType mergeType) {
+ IntegrationStrategy strategy = IntegrationStrategyFactory.create(mergeType, repository, src, toBranch);
+ return strategy.canMerge();
}
@@ -2511,11 +2481,13 @@ public class JGitUtils {
* @param repository
* @param src
* @param toBranch
+ * @param mergeType
+ * Defines the integration strategy to use for merging.
* @param committer
* @param message
* @return the merge result
*/
- public static MergeResult merge(Repository repository, String src, String toBranch,
+ public static MergeResult merge(Repository repository, String src, String toBranch, MergeType mergeType,
PersonIdent committer, String message) {
if (!toBranch.startsWith(Constants.R_REFS)) {
@@ -2523,15 +2495,202 @@ public class JGitUtils {
toBranch = Constants.R_HEADS + toBranch;
}
- RevWalk revWalk = null;
+ IntegrationStrategy strategy = IntegrationStrategyFactory.create(mergeType, repository, src, toBranch);
+ MergeResult mergeResult = strategy.merge(committer, message);
+
+ if (mergeResult.status != MergeStatus.MERGED) {
+ return mergeResult;
+ }
+
try {
- revWalk = new RevWalk(repository);
- RevCommit branchTip = revWalk.lookupCommit(repository.resolve(toBranch));
- RevCommit srcTip = revWalk.lookupCommit(repository.resolve(src));
- if (revWalk.isMergedInto(srcTip, branchTip)) {
- // already merged
- return new MergeResult(MergeStatus.ALREADY_MERGED, null);
+ // Update the integration branch ref
+ RefUpdate mergeRefUpdate = repository.updateRef(toBranch);
+ mergeRefUpdate.setNewObjectId(strategy.getMergeCommit());
+ mergeRefUpdate.setRefLogMessage(strategy.getRefLogMessage(), false);
+ mergeRefUpdate.setExpectedOldObjectId(strategy.branchTip);
+ RefUpdate.Result rc = mergeRefUpdate.update();
+ switch (rc) {
+ case FAST_FORWARD:
+ // successful, clean merge
+ break;
+ default:
+ mergeResult = new MergeResult(MergeStatus.FAILED, null);
+ throw new GitBlitException(MessageFormat.format("Unexpected result \"{0}\" when {1} in {2}",
+ rc.name(), strategy.getOperationMessage(), repository.getDirectory()));
+ }
+ } catch (IOException e) {
+ LOGGER.error("Failed to merge", e);
+ }
+
+ return mergeResult;
+ }
+
+
+ private static abstract class IntegrationStrategy {
+ Repository repository;
+ String src;
+ String toBranch;
+
+ RevWalk revWalk;
+ RevCommit branchTip;
+ RevCommit srcTip;
+
+ RevCommit mergeCommit;
+ String refLogMessage;
+ String operationMessage;
+
+ RevCommit getMergeCommit() {
+ return mergeCommit;
+ }
+
+ String getRefLogMessage() {
+ return refLogMessage;
+ }
+
+ String getOperationMessage() {
+ return operationMessage;
+ }
+
+ IntegrationStrategy(Repository repository, String src, String toBranch) {
+ this.repository = repository;
+ this.src = src;
+ this.toBranch = toBranch;
+ }
+
+ void prepare() throws IOException {
+ if (revWalk == null) revWalk = new RevWalk(repository);
+ ObjectId branchId = repository.resolve(toBranch);
+ if (branchId != null) {
+ branchTip = revWalk.lookupCommit(branchId);
+ }
+ ObjectId srcId = repository.resolve(src);
+ if (srcId != null) {
+ srcTip = revWalk.lookupCommit(srcId);
+ }
+ }
+
+
+ abstract MergeStatus _canMerge() throws IOException;
+
+
+ MergeStatus canMerge() {
+ try {
+ prepare();
+ if (branchTip == null) {
+ return MergeStatus.MISSING_INTEGRATION_BRANCH;
+ }
+ if (srcTip == null) {
+ return MergeStatus.MISSING_SRC_BRANCH;
+ }
+ if (revWalk.isMergedInto(srcTip, branchTip)) {
+ // already merged
+ return MergeStatus.ALREADY_MERGED;
+ }
+ // determined by specific integration strategy
+ return _canMerge();
+
+ } catch (NullPointerException e) {
+ LOGGER.error("Failed to determine canMerge", e);
+ } catch (IOException e) {
+ LOGGER.error("Failed to determine canMerge", e);
+ } finally {
+ if (revWalk != null) {
+ revWalk.close();
+ }
+ }
+
+ return MergeStatus.NOT_MERGEABLE;
+ }
+
+
+ abstract MergeResult _merge(PersonIdent committer, String message) throws IOException;
+
+
+ MergeResult merge(PersonIdent committer, String message) {
+ try {
+ prepare();
+ if (revWalk.isMergedInto(srcTip, branchTip)) {
+ // already merged
+ return new MergeResult(MergeStatus.ALREADY_MERGED, null);
+ }
+ // determined by specific integration strategy
+ return _merge(committer, message);
+
+ } catch (IOException e) {
+ LOGGER.error("Failed to merge", e);
+ } finally {
+ if (revWalk != null) {
+ revWalk.close();
+ }
+ }
+
+ return new MergeResult(MergeStatus.FAILED, null);
+ }
+ }
+
+
+ private static class FastForwardOnly extends IntegrationStrategy {
+ FastForwardOnly(Repository repository, String src, String toBranch) {
+ super(repository, src, toBranch);
+ }
+
+ @Override
+ MergeStatus _canMerge() throws IOException {
+ if (revWalk.isMergedInto(branchTip, srcTip)) {
+ // fast-forward
+ return MergeStatus.MERGEABLE;
+ }
+
+ return MergeStatus.NOT_MERGEABLE;
+ }
+
+ @Override
+ MergeResult _merge(PersonIdent committer, String message) throws IOException {
+ if (! revWalk.isMergedInto(branchTip, srcTip)) {
+ // is not fast-forward
+ return new MergeResult(MergeStatus.FAILED, null);
+ }
+
+ mergeCommit = srcTip;
+ refLogMessage = "merge " + src + ": Fast-forward";
+ operationMessage = MessageFormat.format("fast-forwarding {0} to commit {1}", srcTip.getName(), branchTip.getName());
+
+ return new MergeResult(MergeStatus.MERGED, srcTip.getName());
+ }
+ }
+
+ private static class MergeIfNecessary extends IntegrationStrategy {
+ MergeIfNecessary(Repository repository, String src, String toBranch) {
+ super(repository, src, toBranch);
+ }
+
+ @Override
+ MergeStatus _canMerge() throws IOException {
+ if (revWalk.isMergedInto(branchTip, srcTip)) {
+ // fast-forward
+ return MergeStatus.MERGEABLE;
+ }
+
+ RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
+ boolean canMerge = merger.merge(branchTip, srcTip);
+ if (canMerge) {
+ return MergeStatus.MERGEABLE;
+ }
+
+ return MergeStatus.NOT_MERGEABLE;
+ }
+
+ @Override
+ MergeResult _merge(PersonIdent committer, String message) throws IOException {
+ if (revWalk.isMergedInto(branchTip, srcTip)) {
+ // fast-forward
+ mergeCommit = srcTip;
+ refLogMessage = "merge " + src + ": Fast-forward";
+ operationMessage = MessageFormat.format("fast-forwarding {0} to commit {1}", branchTip.getName(), srcTip.getName());
+
+ return new MergeResult(MergeStatus.MERGED, srcTip.getName());
}
+
RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
boolean merged = merger.merge(branchTip, srcTip);
if (merged) {
@@ -2555,20 +2714,64 @@ public class JGitUtils {
ObjectId mergeCommitId = odi.insert(commitBuilder);
odi.flush();
- // set the merge ref to the merge commit
- RevCommit mergeCommit = revWalk.parseCommit(mergeCommitId);
- RefUpdate mergeRefUpdate = repository.updateRef(toBranch);
- mergeRefUpdate.setNewObjectId(mergeCommitId);
- mergeRefUpdate.setRefLogMessage("commit: " + mergeCommit.getShortMessage(), false);
- RefUpdate.Result rc = mergeRefUpdate.update();
- switch (rc) {
- case FAST_FORWARD:
- // successful, clean merge
- break;
- default:
- throw new GitBlitException(MessageFormat.format("Unexpected result \"{0}\" when merging commit {1} into {2} in {3}",
- rc.name(), srcTip.getName(), branchTip.getName(), repository.getDirectory()));
+ mergeCommit = revWalk.parseCommit(mergeCommitId);
+ refLogMessage = "commit: " + mergeCommit.getShortMessage();
+ operationMessage = MessageFormat.format("merging commit {0} into {1}", srcTip.getName(), branchTip.getName());
+
+ // return the merge commit id
+ return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName());
+ } finally {
+ odi.close();
+ }
+ }
+ return new MergeResult(MergeStatus.FAILED, null);
+ }
+ }
+
+ private static class MergeAlways extends IntegrationStrategy {
+ MergeAlways(Repository repository, String src, String toBranch) {
+ super(repository, src, toBranch);
+ }
+
+ @Override
+ MergeStatus _canMerge() throws IOException {
+ RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
+ boolean canMerge = merger.merge(branchTip, srcTip);
+ if (canMerge) {
+ return MergeStatus.MERGEABLE;
+ }
+
+ return MergeStatus.NOT_MERGEABLE;
+ }
+
+ @Override
+ MergeResult _merge(PersonIdent committer, String message) throws IOException {
+ RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
+ boolean merged = merger.merge(branchTip, srcTip);
+ if (merged) {
+ // create a merge commit and a reference to track the merge commit
+ ObjectId treeId = merger.getResultTreeId();
+ ObjectInserter odi = repository.newObjectInserter();
+ try {
+ // Create a commit object
+ CommitBuilder commitBuilder = new CommitBuilder();
+ commitBuilder.setCommitter(committer);
+ commitBuilder.setAuthor(committer);
+ commitBuilder.setEncoding(Constants.CHARSET);
+ if (StringUtils.isEmpty(message)) {
+ message = MessageFormat.format("merge {0} into {1}", srcTip.getName(), branchTip.getName());
}
+ commitBuilder.setMessage(message);
+ commitBuilder.setParentIds(branchTip.getId(), srcTip.getId());
+ commitBuilder.setTreeId(treeId);
+
+ // Insert the merge commit into the repository
+ ObjectId mergeCommitId = odi.insert(commitBuilder);
+ odi.flush();
+
+ mergeCommit = revWalk.parseCommit(mergeCommitId);
+ refLogMessage = "commit: " + mergeCommit.getShortMessage();
+ operationMessage = MessageFormat.format("merging commit {0} into {1}", srcTip.getName(), branchTip.getName());
// return the merge commit id
return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName());
@@ -2576,17 +2779,27 @@ public class JGitUtils {
odi.close();
}
}
- } catch (IOException e) {
- LOGGER.error("Failed to merge", e);
- } finally {
- if (revWalk != null) {
- revWalk.close();
+
+ return new MergeResult(MergeStatus.FAILED, null);
+ }
+ }
+
+
+ private static class IntegrationStrategyFactory {
+ static IntegrationStrategy create(MergeType mergeType, Repository repository, String src, String toBranch) {
+ switch(mergeType) {
+ case FAST_FORWARD_ONLY:
+ return new FastForwardOnly(repository, src, toBranch);
+ case MERGE_IF_NECESSARY:
+ return new MergeIfNecessary(repository, src, toBranch);
+ case MERGE_ALWAYS:
+ return new MergeAlways(repository, src, toBranch);
}
+ return null;
}
- return new MergeResult(MergeStatus.FAILED, null);
}
-
-
+
+
/**
* Returns the LFS URL for the given oid
* Currently assumes that the Gitblit Filestore is used
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
index a215b4d6..b3cbef82 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -660,6 +660,7 @@ gb.nTotalTickets = {0} total
gb.body = body
gb.mergeSha = mergeSha
gb.mergeTo = merge to
+gb.mergeType = merge type
gb.labels = labels
gb.reviewers = reviewers
gb.voters = voters
@@ -671,6 +672,7 @@ gb.repositoryDoesNotAcceptPatchsets = This repository does not accept patchsets.
gb.serverDoesNotAcceptPatchsets = This server does not accept patchsets.
gb.ticketIsClosed = This ticket is closed.
gb.mergeToDescription = default integration branch for merging ticket patchsets
+gb.mergeTypeDescription = merge a ticket fast-forward only, if necessary, or always with a merge commit to the integration branch
gb.anonymousCanNotPropose = Anonymous users can not propose patchsets.
gb.youDoNotHaveClonePermission = You are not permitted to clone this repository.
gb.myTickets = my tickets
diff --git a/src/main/java/com/gitblit/wicket/pages/BasePage.html b/src/main/java/com/gitblit/wicket/pages/BasePage.html
index 9b026982..0335bfe7 100644
--- a/src/main/java/com/gitblit/wicket/pages/BasePage.html
+++ b/src/main/java/com/gitblit/wicket/pages/BasePage.html
@@ -17,6 +17,7 @@
<link rel="stylesheet" href="fontawesome/css/font-awesome.min.css"/>
<link rel="stylesheet" href="octicons/octicons.css"/>
<link rel="stylesheet" type="text/css" href="gitblit.css"/>
+ <link rel="stylesheet" type="text/css" href="bootstrap-fixes.css"/>
</wicket:head>
<body>
diff --git a/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html b/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html
index 7a55b9f5..2c881efc 100644
--- a/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html
@@ -123,7 +123,8 @@
<div wicket:id="acceptNewTickets"></div>
<div wicket:id="requireApproval"></div>
<div wicket:id="mergeTo"></div>
-
+ <div wicket:id="mergeType"></div>
+
</div>
<!-- federation -->
diff --git a/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java
index 355867b6..61e6b0c9 100644
--- a/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -56,6 +56,7 @@ import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.Constants.CommitMessageRenderer;
import com.gitblit.Constants.FederationStrategy;
+import com.gitblit.Constants.MergeType;
import com.gitblit.Constants.RegistrantType;
import com.gitblit.GitBlitException;
import com.gitblit.Keys;
@@ -459,6 +460,11 @@ public class EditRepositoryPage extends RootSubPage {
getString("gb.mergeToDescription"),
new PropertyModel<String>(repositoryModel, "mergeTo"),
availableBranches));
+ form.add(new ChoiceOption<MergeType>("mergeType",
+ getString("gb.mergeType"),
+ getString("gb.mergeTypeDescription"),
+ new PropertyModel<MergeType>(repositoryModel, "mergeType"),
+ Arrays.asList(MergeType.values())));
//
// RECEIVE
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.java b/src/main/java/com/gitblit/wicket/pages/TicketPage.java
index 25c24738..290d6437 100644
--- a/src/main/java/com/gitblit/wicket/pages/TicketPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.java
@@ -1404,14 +1404,14 @@ public class TicketPage extends RepositoryPage {
boolean allowMerge;
if (repository.requireApproval) {
- // rpeository requires approval
+ // repository requires approval
allowMerge = ticket.isOpen() && ticket.isApproved(patchset);
} else {
- // vetos are binding
+ // vetoes are binding
allowMerge = ticket.isOpen() && !ticket.isVetoed(patchset);
}
- MergeStatus mergeStatus = JGitUtils.canMerge(getRepository(), patchset.tip, ticket.mergeTo);
+ MergeStatus mergeStatus = JGitUtils.canMerge(getRepository(), patchset.tip, ticket.mergeTo, repository.mergeType);
if (allowMerge) {
if (MergeStatus.MERGEABLE == mergeStatus) {
// patchset can be cleanly merged to integration branch OR has already been merged
diff --git a/src/main/java/com/gitblit/wicket/panels/PagerPanel.java b/src/main/java/com/gitblit/wicket/panels/PagerPanel.java
index c80bb73c..3a136f03 100644
--- a/src/main/java/com/gitblit/wicket/panels/PagerPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/PagerPanel.java
@@ -48,7 +48,7 @@ public class PagerPanel extends Panel {
deltas = new int[] { -2, -1, 0, 1, 2 };
}
- if (totalPages > 0) {
+ if (totalPages > 0 && currentPage > 1) {
pages.add(new PageObject("\u2190", currentPage - 1));
}
for (int delta : deltas) {
@@ -57,7 +57,7 @@ public class PagerPanel extends Panel {
pages.add(new PageObject("" + page, page));
}
}
- if (totalPages > 0) {
+ if (totalPages > 0 && currentPage < totalPages) {
pages.add(new PageObject("\u2192", currentPage + 1));
}
@@ -75,6 +75,7 @@ public class PagerPanel extends Panel {
item.add(link);
if (pageItem.page == currentPage || pageItem.page < 1 || pageItem.page > totalPages) {
WicketUtils.setCssClass(item, "disabled");
+ link.setEnabled(false);
}
}
};
diff --git a/src/main/resources/bootstrap-fixes.css b/src/main/resources/bootstrap-fixes.css
new file mode 100644
index 00000000..c9b6154b
--- /dev/null
+++ b/src/main/resources/bootstrap-fixes.css
@@ -0,0 +1,25 @@
+/**
+ * Disabled links in a PagerPanel. Bootstrap 2.0.4 only handles <a>, but not <span>. Wicket renders disabled links as spans.
+ * The .pagination rules here are identical to the ones for <a> in bootstrap.css, but for <span>.
+ */
+.pagination span {
+ float: left;
+ padding: 0 14px;
+ line-height: 34px;
+ text-decoration: none;
+ border: 1px solid #ddd;
+ border-left-width: 0;
+}
+
+.pagination li:first-child span {
+ border-left-width: 1px;
+ -webkit-border-radius: 3px 0 0 3px;
+ -moz-border-radius: 3px 0 0 3px;
+ border-radius: 3px 0 0 3px;
+}
+
+.pagination li:last-child span {
+ -webkit-border-radius: 0 3px 3px 0;
+ -moz-border-radius: 0 3px 3px 0;
+ border-radius: 0 3px 3px 0;
+}
diff --git a/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java b/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java
index 84dd138d..2ade6819 100644
--- a/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java
+++ b/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java
@@ -16,17 +16,26 @@
*/
package com.gitblit.tests;
+import static org.junit.Assume.*;
+
import java.io.File;
-import java.io.FileInputStream;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.io.FileUtils;
+import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
import com.gitblit.Constants.AccountType;
import com.gitblit.IStoredSettings;
@@ -43,9 +52,24 @@ import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
+import com.unboundid.ldap.listener.InMemoryDirectoryServerSnapshot;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
+import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedRequest;
+import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedResult;
+import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchEntry;
+import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchRequest;
+import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
+import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSimpleBindResult;
+import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
+import com.unboundid.ldap.sdk.BindRequest;
+import com.unboundid.ldap.sdk.BindResult;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.LDAPResult;
+import com.unboundid.ldap.sdk.OperationType;
+import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchScope;
+import com.unboundid.ldap.sdk.SimpleBindRequest;
import com.unboundid.ldif.LDIFReader;
/**
@@ -55,19 +79,71 @@ import com.unboundid.ldif.LDIFReader;
* @author jcrygier
*
*/
+@RunWith(Parameterized.class)
public class LdapAuthenticationTest extends GitblitUnitTest {
- @Rule
- public TemporaryFolder folder = new TemporaryFolder();
private static final String RESOURCE_DIR = "src/test/resources/ldap/";
+ private static final String DIRECTORY_MANAGER = "cn=Directory Manager";
+ private static final String USER_MANAGER = "cn=UserManager";
+ private static final String ACCOUNT_BASE = "OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain";
+ private static final String GROUP_BASE = "OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain";
+
+
+ /**
+ * Enumeration of different test modes, representing different use scenarios.
+ * With ANONYMOUS anonymous binds are used to search LDAP.
+ * DS_MANAGER will use a DIRECTORY_MANAGER to search LDAP. Normal users are prohibited to search the DS.
+ * With USR_MANAGER, a USER_MANAGER account is used to search in LDAP. This account can only search users
+ * but not groups. Normal users can search groups, though.
+ *
+ */
+ enum AuthMode {
+ ANONYMOUS(1389),
+ DS_MANAGER(2389),
+ USR_MANAGER(3389);
+
+
+ private int ldapPort;
+ private InMemoryDirectoryServer ds;
+ private InMemoryDirectoryServerSnapshot dsSnapshot;
+
+ AuthMode(int port) {
+ this.ldapPort = port;
+ }
+
+ int ldapPort() {
+ return this.ldapPort;
+ }
+
+ void setDS(InMemoryDirectoryServer ds) {
+ if (this.ds == null) {
+ this.ds = ds;
+ this.dsSnapshot = ds.createSnapshot();
+ };
+ }
+
+ InMemoryDirectoryServer getDS() {
+ return ds;
+ }
+
+ void restoreSnapshot() {
+ ds.restoreSnapshot(dsSnapshot);
+ }
+ };
+
- private File usersConf;
- private LdapAuthProvider ldap;
+ @Parameter
+ public AuthMode authMode;
- static int ldapPort = 1389;
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
- private static InMemoryDirectoryServer ds;
+ private File usersConf;
+
+
+
+ private LdapAuthProvider ldap;
private IUserManager userManager;
@@ -75,21 +151,82 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
private MemorySettings settings;
+
+ /**
+ * Run the tests with each authentication scenario once.
+ */
+ @Parameters(name = "{0}")
+ public static Collection<Object[]> data() {
+ return Arrays.asList(new Object[][] { {AuthMode.ANONYMOUS}, {AuthMode.DS_MANAGER}, {AuthMode.USR_MANAGER} });
+ }
+
+
+
+ /**
+ * Create three different in memory DS.
+ *
+ * Each DS has a different configuration:
+ * The first allows anonymous binds.
+ * The second requires authentication for all operations. It will only allow the DIRECTORY_MANAGER account
+ * to search for users and groups.
+ * The third one is like the second, but it allows users to search for users and groups, and restricts the
+ * USER_MANAGER from searching for groups.
+ */
@BeforeClass
- public static void createInMemoryLdapServer() throws Exception {
+ public static void init() throws Exception {
+ InMemoryDirectoryServer ds;
+ InMemoryDirectoryServerConfig config = createInMemoryLdapServerConfig(AuthMode.ANONYMOUS);
+ config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", AuthMode.ANONYMOUS.ldapPort()));
+ ds = createInMemoryLdapServer(config);
+ AuthMode.ANONYMOUS.setDS(ds);
+
+
+ config = createInMemoryLdapServerConfig(AuthMode.DS_MANAGER);
+ config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", AuthMode.DS_MANAGER.ldapPort()));
+ config.setAuthenticationRequiredOperationTypes(EnumSet.allOf(OperationType.class));
+ ds = createInMemoryLdapServer(config);
+ AuthMode.DS_MANAGER.setDS(ds);
+
+
+ config = createInMemoryLdapServerConfig(AuthMode.USR_MANAGER);
+ config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", AuthMode.USR_MANAGER.ldapPort()));
+ config.setAuthenticationRequiredOperationTypes(EnumSet.allOf(OperationType.class));
+ ds = createInMemoryLdapServer(config);
+ AuthMode.USR_MANAGER.setDS(ds);
+
+ }
+
+ @AfterClass
+ public static void destroy() throws Exception {
+ for (AuthMode am : AuthMode.values()) {
+ am.getDS().shutDown(true);
+ }
+ }
+
+ public static InMemoryDirectoryServer createInMemoryLdapServer(InMemoryDirectoryServerConfig config) throws Exception {
+ InMemoryDirectoryServer imds = new InMemoryDirectoryServer(config);
+ imds.importFromLDIF(true, RESOURCE_DIR + "sampledata.ldif");
+ imds.startListening();
+ return imds;
+ }
+
+ public static InMemoryDirectoryServerConfig createInMemoryLdapServerConfig(AuthMode authMode) throws Exception {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=MyDomain");
- config.addAdditionalBindCredentials("cn=Directory Manager", "password");
- config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", ldapPort));
+ config.addAdditionalBindCredentials(DIRECTORY_MANAGER, "password");
+ config.addAdditionalBindCredentials(USER_MANAGER, "passwd");
config.setSchema(null);
- ds = new InMemoryDirectoryServer(config);
- ds.startListening();
+ config.addInMemoryOperationInterceptor(new AccessInterceptor(authMode));
+
+ return config;
}
+
+
@Before
- public void init() throws Exception {
- ds.clear();
- ds.importFromLDIF(true, new LDIFReader(new FileInputStream(RESOURCE_DIR + "sampledata.ldif")));
+ public void setup() throws Exception {
+ authMode.restoreSnapshot();
+
usersConf = folder.newFile("users.conf");
FileUtils.copyFile(new File(RESOURCE_DIR + "users.conf"), usersConf);
settings = getSettings();
@@ -117,15 +254,30 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
private MemorySettings getSettings() {
Map<String, Object> backingMap = new HashMap<String, Object>();
backingMap.put(Keys.realm.userService, usersConf.getAbsolutePath());
- backingMap.put(Keys.realm.ldap.server, "ldap://localhost:" + ldapPort);
-// backingMap.put(Keys.realm.ldap.domain, "");
- backingMap.put(Keys.realm.ldap.username, "cn=Directory Manager");
- backingMap.put(Keys.realm.ldap.password, "password");
-// backingMap.put(Keys.realm.ldap.backingUserService, "users.conf");
+ switch(authMode) {
+ case ANONYMOUS:
+ backingMap.put(Keys.realm.ldap.server, "ldap://localhost:" + authMode.ldapPort());
+ backingMap.put(Keys.realm.ldap.username, "");
+ backingMap.put(Keys.realm.ldap.password, "");
+ break;
+ case DS_MANAGER:
+ backingMap.put(Keys.realm.ldap.server, "ldap://localhost:" + authMode.ldapPort());
+ backingMap.put(Keys.realm.ldap.username, DIRECTORY_MANAGER);
+ backingMap.put(Keys.realm.ldap.password, "password");
+ break;
+ case USR_MANAGER:
+ backingMap.put(Keys.realm.ldap.server, "ldap://localhost:" + authMode.ldapPort());
+ backingMap.put(Keys.realm.ldap.username, USER_MANAGER);
+ backingMap.put(Keys.realm.ldap.password, "passwd");
+ break;
+ default:
+ throw new RuntimeException("Unimplemented AuthMode case!");
+
+ }
backingMap.put(Keys.realm.ldap.maintainTeams, "true");
- backingMap.put(Keys.realm.ldap.accountBase, "OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain");
+ backingMap.put(Keys.realm.ldap.accountBase, ACCOUNT_BASE);
backingMap.put(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
- backingMap.put(Keys.realm.ldap.groupBase, "OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain");
+ backingMap.put(Keys.realm.ldap.groupBase, GROUP_BASE);
backingMap.put(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))");
backingMap.put(Keys.realm.ldap.admins, "UserThree @Git_Admins \"@Git Admins\"");
backingMap.put(Keys.realm.ldap.displayName, "displayName");
@@ -136,6 +288,8 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
return ms;
}
+
+
@Test
public void testAuthenticate() {
UserModel userOneModel = ldap.authenticate("UserOne", "userOnePassword".toCharArray());
@@ -159,6 +313,13 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
assertNotNull(userThreeModel.getTeam("git_users"));
assertNull(userThreeModel.getTeam("git_admins"));
assertTrue(userThreeModel.canAdmin);
+
+ UserModel userFourModel = ldap.authenticate("UserFour", "userFourPassword".toCharArray());
+ assertNotNull(userFourModel);
+ assertNotNull(userFourModel.getTeam("git_users"));
+ assertNull(userFourModel.getTeam("git_admins"));
+ assertNull(userFourModel.getTeam("git admins"));
+ assertFalse(userFourModel.canAdmin);
}
@Test
@@ -204,13 +365,13 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
@Test
public void checkIfUsersConfContainsAllUsersFromSampleDataLdif() throws Exception {
- SearchResult searchResult = ds.search("OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain", SearchScope.SUB, "objectClass=person");
+ SearchResult searchResult = getDS().search(ACCOUNT_BASE, SearchScope.SUB, "objectClass=person");
assertEquals("Number of ldap users in gitblit user model", searchResult.getEntryCount(), countLdapUsersInUserManager());
}
@Test
public void addingUserInLdapShouldNotUpdateGitBlitUsersAndGroups() throws Exception {
- ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "adduser.ldif"));
+ getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "adduser.ldif"));
ldap.sync();
assertEquals("Number of ldap users in gitblit user model", 5, countLdapUsersInUserManager());
}
@@ -218,22 +379,25 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
@Test
public void addingUserInLdapShouldUpdateGitBlitUsersAndGroups() throws Exception {
settings.put(Keys.realm.ldap.synchronize, "true");
- ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "adduser.ldif"));
+ getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "adduser.ldif"));
ldap.sync();
assertEquals("Number of ldap users in gitblit user model", 6, countLdapUsersInUserManager());
}
@Test
public void addingGroupsInLdapShouldNotUpdateGitBlitUsersAndGroups() throws Exception {
- ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif"));
+ getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif"));
ldap.sync();
assertEquals("Number of ldap groups in gitblit team model", 0, countLdapTeamsInUserManager());
}
@Test
public void addingGroupsInLdapShouldUpdateGitBlitUsersAndGroups() throws Exception {
+ // This test only makes sense if the authentication mode allows for synchronization.
+ assumeTrue(authMode == AuthMode.ANONYMOUS || authMode == AuthMode.DS_MANAGER);
+
settings.put(Keys.realm.ldap.synchronize, "true");
- ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif"));
+ getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif"));
ldap.sync();
assertEquals("Number of ldap groups in gitblit team model", 1, countLdapTeamsInUserManager());
}
@@ -261,11 +425,21 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
assertNotNull(userThreeModel.getTeam("git_users"));
assertNull(userThreeModel.getTeam("git_admins"));
assertTrue(userThreeModel.canAdmin);
+
+ UserModel userFourModel = auth.authenticate("UserFour", "userFourPassword".toCharArray(), null);
+ assertNotNull(userFourModel);
+ assertNotNull(userFourModel.getTeam("git_users"));
+ assertNull(userFourModel.getTeam("git_admins"));
+ assertNull(userFourModel.getTeam("git admins"));
+ assertFalse(userFourModel.canAdmin);
}
@Test
public void testBindWithUser() {
- settings.put(Keys.realm.ldap.bindpattern, "CN=${username},OU=US,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain");
+ // This test only makes sense if the user is not prevented from reading users and teams.
+ assumeTrue(authMode != AuthMode.DS_MANAGER);
+
+ settings.put(Keys.realm.ldap.bindpattern, "CN=${username},OU=US," + ACCOUNT_BASE);
settings.put(Keys.realm.ldap.username, "");
settings.put(Keys.realm.ldap.password, "");
@@ -276,6 +450,12 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
assertNull(userOneModelFailedAuth);
}
+
+ private InMemoryDirectoryServer getDS()
+ {
+ return authMode.getDS();
+ }
+
private int countLdapUsersInUserManager() {
int ldapAccountCount = 0;
for (UserModel userModel : userManager.getAllUsers()) {
@@ -296,4 +476,120 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
return ldapAccountCount;
}
+
+
+
+ /**
+ * Operation interceptor for the in memory DS. This interceptor
+ * implements access restrictions for certain user/DN combinations.
+ *
+ * The USER_MANAGER is only allowed to search for users, but not for groups.
+ * This is to test the original behaviour where the teams were searched under
+ * the user binding.
+ * When running in a DIRECTORY_MANAGER scenario, only the manager account
+ * is allowed to search for users and groups, while a normal user may not do so.
+ * This tests the scenario where a normal user cannot read teams and thus the
+ * manager account needs to be used for all searches.
+ *
+ */
+ private static class AccessInterceptor extends InMemoryOperationInterceptor {
+ AuthMode authMode;
+ Map<Long,String> lastSuccessfulBindDN = new HashMap<>();
+ Map<Long,Boolean> resultProhibited = new HashMap<>();
+
+ public AccessInterceptor(AuthMode authMode) {
+ this.authMode = authMode;
+ }
+
+
+ @Override
+ public void processSimpleBindResult(InMemoryInterceptedSimpleBindResult bind) {
+ BindResult result = bind.getResult();
+ if (result.getResultCode() == ResultCode.SUCCESS) {
+ BindRequest bindRequest = bind.getRequest();
+ lastSuccessfulBindDN.put(bind.getConnectionID(), ((SimpleBindRequest)bindRequest).getBindDN());
+ resultProhibited.remove(bind.getConnectionID());
+ }
+ }
+
+
+
+ @Override
+ public void processSearchRequest(InMemoryInterceptedSearchRequest request) throws LDAPException {
+ String bindDN = getLastBindDN(request);
+
+ if (USER_MANAGER.equals(bindDN)) {
+ if (request.getRequest().getBaseDN().endsWith(GROUP_BASE)) {
+ throw new LDAPException(ResultCode.NO_SUCH_OBJECT);
+ }
+ }
+ else if(authMode == AuthMode.DS_MANAGER && !DIRECTORY_MANAGER.equals(bindDN)) {
+ throw new LDAPException(ResultCode.NO_SUCH_OBJECT);
+ }
+ }
+
+
+ @Override
+ public void processSearchEntry(InMemoryInterceptedSearchEntry entry) {
+ String bindDN = getLastBindDN(entry);
+
+ boolean prohibited = false;
+
+ if (USER_MANAGER.equals(bindDN)) {
+ if (entry.getSearchEntry().getDN().endsWith(GROUP_BASE)) {
+ prohibited = true;
+ }
+ }
+ else if(authMode == AuthMode.DS_MANAGER && !DIRECTORY_MANAGER.equals(bindDN)) {
+ prohibited = true;
+ }
+
+ if (prohibited) {
+ // Found entry prohibited for bound user. Setting entry to null.
+ entry.setSearchEntry(null);
+ resultProhibited.put(entry.getConnectionID(), Boolean.TRUE);
+ }
+ }
+
+ @Override
+ public void processSearchResult(InMemoryInterceptedSearchResult result) {
+ String bindDN = getLastBindDN(result);
+
+ boolean prohibited = false;
+
+ Boolean rspb = resultProhibited.get(result.getConnectionID());
+ if (USER_MANAGER.equals(bindDN)) {
+ if (rspb != null && rspb) {
+ prohibited = true;
+ }
+ }
+ else if(authMode == AuthMode.DS_MANAGER && !DIRECTORY_MANAGER.equals(bindDN)) {
+ if (rspb != null && rspb) {
+ prohibited = true;
+ }
+ }
+
+ if (prohibited) {
+ // Result prohibited for bound user. Returning error
+ result.setResult(new LDAPResult(result.getMessageID(), ResultCode.INSUFFICIENT_ACCESS_RIGHTS));
+ resultProhibited.remove(result.getConnectionID());
+ }
+ }
+
+ private String getLastBindDN(InMemoryInterceptedResult result) {
+ String bindDN = lastSuccessfulBindDN.get(result.getConnectionID());
+ if (bindDN == null) {
+ return "UNKNOWN";
+ }
+ return bindDN;
+ }
+ private String getLastBindDN(InMemoryInterceptedRequest request) {
+ String bindDN = lastSuccessfulBindDN.get(request.getConnectionID());
+ if (bindDN == null) {
+ return "UNKNOWN";
+ }
+ return bindDN;
+ }
+ }
+
}