Merge branch 'rcaa-master' into master.
diff --git a/src/main/distrib/data/defaults.properties b/src/main/distrib/data/defaults.properties index 0c7d6cd..0416634 100644 --- a/src/main/distrib/data/defaults.properties +++ b/src/main/distrib/data/defaults.properties
@@ -567,6 +567,21 @@ # 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.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 @@ # 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.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 6232552..ab503bd 100644 --- a/src/main/java/com/gitblit/Constants.java +++ b/src/main/java/com/gitblit/Constants.java
@@ -62,6 +62,12 @@ public static final String GIT_PATH = "/git/"; public static final String REGEX_SHA256 = "[a-fA-F0-9]{64}"; + + /** + * This regular expression is used when searching for "mentions" in tickets + * (when someone writes @thisOtherUser) + */ + public static final String REGEX_TICKET_MENTION = "\\B@(?<user>[^\\s]+)\\b"; public static final String ZIP_PATH = "/zip/"; @@ -639,6 +645,37 @@ } } + /** + * 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 b7efd4a..c31694b 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.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 @@ 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"); @@ -163,6 +171,8 @@ final Map<String, TeamModel> userTeams = new HashMap<String, TeamModel>(); for (UserModel user : ldapUsers.values()) { for (TeamModel userTeam : user.teams) { + // Is this an administrative team? + setAdminAttribute(userTeam); userTeams.put(userTeam.name, userTeam); } } @@ -179,66 +189,6 @@ } } - 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. @@ -290,10 +240,7 @@ public boolean supportsRoleChanges(UserModel user, Role role) { if (Role.ADMIN == role) { if (!supportsTeamMembershipChanges()) { - List<String> admins = settings.getStrings(Keys.realm.ldap.admins); - if (admins.contains(user.username)) { - return false; - } + return false; } } return true; @@ -303,10 +250,7 @@ public boolean supportsRoleChanges(TeamModel team, Role role) { if (Role.ADMIN == role) { if (!supportsTeamMembershipChanges()) { - List<String> admins = settings.getStrings(Keys.realm.ldap.admins); - if (admins.contains("@" + team.name)) { - return false; - } + return false; } } return true; @@ -321,23 +265,26 @@ public UserModel authenticate(String username, char[] password) { String simpleUsername = getSimpleUsername(username); - LDAPConnection ldapConnection = getLdapConnection(); - if (ldapConnection != null) { + LdapConnection ldapConnection = new LdapConnection(); + if (ldapConnection.connect()) { + + // 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; + } + + try { - boolean alreadyAuthenticated = false; - - String bindPattern = settings.getString(Keys.realm.ldap.bindpattern, ""); - if (!StringUtils.isEmpty(bindPattern)) { - try { - String bindUser = StringUtils.replace(bindPattern, "${username}", escapeLDAPSearchFilter(simpleUsername)); - ldapConnection.bind(bindUser, new String(password)); - - alreadyAuthenticated = true; - } catch (LDAPException e) { - return null; - } - } - // Find the logging in user's DN String accountBase = settings.getString(Keys.realm.ldap.accountBase, ""); String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))"); @@ -348,7 +295,7 @@ 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; @@ -374,6 +321,8 @@ if (!supportsTeamMembershipChanges()) { for (TeamModel userTeam : user.teams) { + // Is this an administrative team? + setAdminAttribute(userTeam); updateTeam(userTeam); } } @@ -404,10 +353,7 @@ if (!ArrayUtils.isEmpty(admins)) { user.canAdmin = false; for (String admin : admins) { - if (admin.startsWith("@") && user.isTeamMember(admin.substring(1))) { - // admin team - user.canAdmin = true; - } else if (user.getName().equalsIgnoreCase(admin)) { + if (user.getName().equalsIgnoreCase(admin)) { // admin user user.canAdmin = true; } @@ -416,6 +362,30 @@ } } + /** + * Set the canAdmin attribute for team 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 team + */ + private void setAdminAttribute(TeamModel team) { + 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)) { + team.canAdmin = false; + for (String admin : admins) { + if (admin.startsWith("@") && team.name.equalsIgnoreCase(admin.substring(1))) { + // admin team + team.canAdmin = true; + } + } + } + } + } + private void setUserAttributes(UserModel user, SearchResultEntry userEntry) { // Is this user an admin? setAdminAttribute(user); @@ -462,7 +432,7 @@ } } - 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 +449,7 @@ 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 +466,12 @@ } } - 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); @@ -511,6 +481,7 @@ TeamModel teamModel = userManager.getTeamModel(teamName); if (teamModel == null) { teamModel = createTeamFromLdap(teamEntry); + setAdminAttribute(teamModel); userManager.updateTeamModel(teamModel); } } @@ -519,6 +490,30 @@ 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 +522,21 @@ 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); + SearchResult result = ldapConnection.search(searchRequest); + if (result.getResultCode() != ResultCode.SUCCESS) { + return null; } - if (attributes != null) { - searchRequest.setAttributes(attributes); - } - 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 +554,7 @@ } // 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 +594,225 @@ } } + + + 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 33fa470..4a09139 100644 --- a/src/main/java/com/gitblit/git/PatchsetReceivePack.java +++ b/src/main/java/com/gitblit/git/PatchsetReceivePack.java
@@ -599,7 +599,7 @@ } // 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 @@ 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 e9bf5b8..2be6587 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.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 @@ 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 @@ 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 @@ } 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(); - } - } - 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))); + 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(); + } + } + 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 a81c622..67ee1c7 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.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 boolean acceptNewTickets; public boolean requireApproval; public String mergeTo; + public MergeType mergeType; public transient boolean isCollectingGarbage; public Date lastGC; @@ -111,6 +113,7 @@ 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/models/TicketModel.java b/src/main/java/com/gitblit/models/TicketModel.java index d534589..65e29dc 100644 --- a/src/main/java/com/gitblit/models/TicketModel.java +++ b/src/main/java/com/gitblit/models/TicketModel.java
@@ -43,6 +43,8 @@ import org.eclipse.jgit.util.RelativeDateFormatter; +import com.gitblit.Constants; + /** * The Gitblit Ticket model, its component classes, and enums. * @@ -773,10 +775,10 @@ } try { - Pattern mentions = Pattern.compile("\\s@([A-Za-z0-9-_]+)"); + Pattern mentions = Pattern.compile(Constants.REGEX_TICKET_MENTION); Matcher m = mentions.matcher(text); while (m.find()) { - String username = m.group(1); + String username = m.group("user"); plusList(Field.mentions, username); } } catch (Exception e) {
diff --git a/src/main/java/com/gitblit/service/MailService.java b/src/main/java/com/gitblit/service/MailService.java index ec3a84c..58acc9c 100644 --- a/src/main/java/com/gitblit/service/MailService.java +++ b/src/main/java/com/gitblit/service/MailService.java
@@ -17,6 +17,7 @@ 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.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 @@ 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/tickets/TicketNotifier.java b/src/main/java/com/gitblit/tickets/TicketNotifier.java index 8c7fe6d..b913db2 100644 --- a/src/main/java/com/gitblit/tickets/TicketNotifier.java +++ b/src/main/java/com/gitblit/tickets/TicketNotifier.java
@@ -573,10 +573,10 @@ // cc users mentioned in last comment Change lastChange = ticket.changes.get(ticket.changes.size() - 1); if (lastChange.hasComment()) { - Pattern p = Pattern.compile("\\s@([A-Za-z0-9-_]+)"); + Pattern p = Pattern.compile(Constants.REGEX_TICKET_MENTION); Matcher m = p.matcher(lastChange.comment.text); while (m.find()) { - String username = m.group(); + String username = m.group("user"); ccs.add(username); } }
diff --git a/src/main/java/com/gitblit/utils/ArrayUtils.java b/src/main/java/com/gitblit/utils/ArrayUtils.java index 1402ad5..b850ccc 100644 --- a/src/main/java/com/gitblit/utils/ArrayUtils.java +++ b/src/main/java/com/gitblit/utils/ArrayUtils.java
@@ -42,7 +42,7 @@ } 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 a3963f5..53b8de1 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.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 @@ } protected CommitCache() { - cache = new ConcurrentHashMap<String, ObjectCache<List<RepositoryCommit>>>(); + cache = new HashMap<>(); } /** @@ -93,7 +93,9 @@ * */ public void clear() { - cache.clear(); + synchronized (cache) { + cache.clear(); + } } /** @@ -103,8 +105,11 @@ */ 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 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 @@ 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 @@ */ 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 @@ */ 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 @@ * @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 a02fc3f..0eea1d6 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.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 @@ * @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 @@ * @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 @@ 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,9 @@ 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()); @@ -2576,17 +2724,82 @@ odi.close(); } } - } catch (IOException e) { - LOGGER.error("Failed to merge", e); - } finally { - if (revWalk != null) { - revWalk.close(); - } + return new MergeResult(MergeStatus.FAILED, null); } - 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()); + } finally { + odi.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; + } + } + + /** * Returns the LFS URL for the given oid * Currently assumes that the Gitblit Filestore is used
diff --git a/src/main/java/com/gitblit/utils/MarkdownUtils.java b/src/main/java/com/gitblit/utils/MarkdownUtils.java index e0c9dd4..8371b3c 100644 --- a/src/main/java/com/gitblit/utils/MarkdownUtils.java +++ b/src/main/java/com/gitblit/utils/MarkdownUtils.java
@@ -30,6 +30,7 @@ import org.pegdown.PegDownProcessor; import org.pegdown.ast.RootNode; +import com.gitblit.Constants; import com.gitblit.IStoredSettings; import com.gitblit.Keys; import com.gitblit.wicket.MarkupProcessor.WorkaroundHtmlSerializer; @@ -137,8 +138,8 @@ String canonicalUrl = settings.getString(Keys.web.canonicalUrl, "https://localhost:8443"); // emphasize and link mentions - String mentionReplacement = String.format(" **[@$1](%1s/user/$1)**", canonicalUrl); - text = text.replaceAll("\\s@([A-Za-z0-9-_]+)", mentionReplacement); + String mentionReplacement = String.format("**[@${user}](%1s/user/${user})**", canonicalUrl); + text = text.replaceAll(Constants.REGEX_TICKET_MENTION, mentionReplacement); // link ticket refs String ticketReplacement = MessageFormat.format("$1[#$2]({0}/tickets?r={1}&h=$2)$3", canonicalUrl, repositoryName);
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties index a215b4d..b3cbef8 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -660,6 +660,7 @@ 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.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/GitBlitWebApp_nl.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_nl.properties index f43b8f5..f71d67d 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_nl.properties +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_nl.properties
@@ -476,7 +476,7 @@ gb.pushedNewTag = push nieuwe tag gb.createdNewTag = nieuwe tag gemaakt gb.deletedTag = tag verwijderd -gb.pushedNewBranch = push neuwe branch +gb.pushedNewBranch = push nieuwe branch gb.createdNewBranch = nieuwe branch gemaakt gb.deletedBranch = branch verwijderd gb.createdNewPullRequest = pull verzoek gemaakt
diff --git a/src/main/java/com/gitblit/wicket/pages/BasePage.html b/src/main/java/com/gitblit/wicket/pages/BasePage.html index b998428..4dbc2e5 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 7a55b9f..2c881ef 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 6bcf6f5..bf3eea8 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.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; @@ -458,6 +459,11 @@ 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 cd049f4..e213396 100644 --- a/src/main/java/com/gitblit/wicket/pages/TicketPage.java +++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.java
@@ -1405,14 +1405,14 @@ 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 2d774c4..d1214ca 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 @@ 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 @@ 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 @@ 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 0000000..c9b6154 --- /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 84dd138..b7a77fc 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.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 @@ * @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"; - private File usersConf; - private LdapAuthProvider ldap; + /** + * 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); - static int ldapPort = 1389; - private static InMemoryDirectoryServer ds; + 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); + } + }; + + + + @Parameter + public AuthMode authMode; + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + private File usersConf; + + + + private LdapAuthProvider ldap; private IUserManager userManager; @@ -75,21 +151,82 @@ private MemorySettings settings; - @BeforeClass - public static void createInMemoryLdapServer() throws Exception { - InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=MyDomain"); - config.addAdditionalBindCredentials("cn=Directory Manager", "password"); - config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", ldapPort)); - config.setSchema(null); - ds = new InMemoryDirectoryServer(config); - ds.startListening(); + /** + * 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 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(DIRECTORY_MANAGER, "password"); + config.addAdditionalBindCredentials(USER_MANAGER, "passwd"); + config.setSchema(null); + + 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 @@ 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,13 +288,14 @@ return ms; } + + @Test public void testAuthenticate() { UserModel userOneModel = ldap.authenticate("UserOne", "userOnePassword".toCharArray()); assertNotNull(userOneModel); assertNotNull(userOneModel.getTeam("git_admins")); assertNotNull(userOneModel.getTeam("git_users")); - assertTrue(userOneModel.canAdmin); UserModel userOneModelFailedAuth = ldap.authenticate("UserOne", "userTwoPassword".toCharArray()); assertNull(userOneModelFailedAuth); @@ -152,13 +305,101 @@ assertNotNull(userTwoModel.getTeam("git_users")); assertNull(userTwoModel.getTeam("git_admins")); assertNotNull(userTwoModel.getTeam("git admins")); - assertTrue(userTwoModel.canAdmin); UserModel userThreeModel = ldap.authenticate("UserThree", "userThreePassword".toCharArray()); assertNotNull(userThreeModel); assertNotNull(userThreeModel.getTeam("git_users")); assertNull(userThreeModel.getTeam("git_admins")); + + UserModel userFourModel = ldap.authenticate("UserFour", "userFourPassword".toCharArray()); + assertNotNull(userFourModel); + assertNotNull(userFourModel.getTeam("git_users")); + assertNull(userFourModel.getTeam("git_admins")); + assertNull(userFourModel.getTeam("git admins")); + } + + @Test + public void testAdminPropertyTeamsInLdap() { + UserModel userOneModel = ldap.authenticate("UserOne", "userOnePassword".toCharArray()); + assertNotNull(userOneModel); + assertNotNull(userOneModel.getTeam("git_admins")); + assertNull(userOneModel.getTeam("git admins")); + assertNotNull(userOneModel.getTeam("git_users")); + assertFalse(userOneModel.canAdmin); + assertTrue(userOneModel.canAdmin()); + assertTrue(userOneModel.getTeam("git_admins").canAdmin); + assertFalse(userOneModel.getTeam("git_users").canAdmin); + + UserModel userTwoModel = ldap.authenticate("UserTwo", "userTwoPassword".toCharArray()); + assertNotNull(userTwoModel); + assertNotNull(userTwoModel.getTeam("git_users")); + assertNull(userTwoModel.getTeam("git_admins")); + assertNotNull(userTwoModel.getTeam("git admins")); + assertFalse(userTwoModel.canAdmin); + assertTrue(userTwoModel.canAdmin()); + assertTrue(userTwoModel.getTeam("git admins").canAdmin); + assertFalse(userTwoModel.getTeam("git_users").canAdmin); + + UserModel userThreeModel = ldap.authenticate("UserThree", "userThreePassword".toCharArray()); + assertNotNull(userThreeModel); + assertNotNull(userThreeModel.getTeam("git_users")); + assertNull(userThreeModel.getTeam("git_admins")); + assertNull(userThreeModel.getTeam("git admins")); assertTrue(userThreeModel.canAdmin); + assertTrue(userThreeModel.canAdmin()); + assertFalse(userThreeModel.getTeam("git_users").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); + assertFalse(userFourModel.canAdmin()); + assertFalse(userFourModel.getTeam("git_users").canAdmin); + } + + @Test + public void testAdminPropertyTeamsNotInLdap() { + settings.put(Keys.realm.ldap.maintainTeams, "false"); + + UserModel userOneModel = ldap.authenticate("UserOne", "userOnePassword".toCharArray()); + assertNotNull(userOneModel); + assertNotNull(userOneModel.getTeam("git_admins")); + assertNull(userOneModel.getTeam("git admins")); + assertNotNull(userOneModel.getTeam("git_users")); + assertTrue(userOneModel.canAdmin); + assertTrue(userOneModel.canAdmin()); + assertFalse(userOneModel.getTeam("git_admins").canAdmin); + assertFalse(userOneModel.getTeam("git_users").canAdmin); + + UserModel userTwoModel = ldap.authenticate("UserTwo", "userTwoPassword".toCharArray()); + assertNotNull(userTwoModel); + assertNotNull(userTwoModel.getTeam("git_users")); + assertNull(userTwoModel.getTeam("git_admins")); + assertNotNull(userTwoModel.getTeam("git admins")); + assertFalse(userTwoModel.canAdmin); + assertTrue(userTwoModel.canAdmin()); + assertTrue(userTwoModel.getTeam("git admins").canAdmin); + assertFalse(userTwoModel.getTeam("git_users").canAdmin); + + UserModel userThreeModel = ldap.authenticate("UserThree", "userThreePassword".toCharArray()); + assertNotNull(userThreeModel); + assertNotNull(userThreeModel.getTeam("git_users")); + assertNull(userThreeModel.getTeam("git_admins")); + assertNull(userThreeModel.getTeam("git admins")); + assertFalse(userThreeModel.canAdmin); + assertFalse(userThreeModel.canAdmin()); + assertFalse(userThreeModel.getTeam("git_users").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); + assertFalse(userFourModel.canAdmin()); + assertFalse(userFourModel.getTeam("git_users").canAdmin); } @Test @@ -204,13 +445,13 @@ @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,33 +459,126 @@ @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 { + public void addingGroupsInLdapShouldUpdateGitBlitUsersNotGroups2() throws Exception { settings.put(Keys.realm.ldap.synchronize, "true"); - ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif")); + settings.put(Keys.realm.ldap.maintainTeams, "false"); + getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "adduser.ldif")); + getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif")); + ldap.sync(); + assertEquals("Number of ldap users in gitblit user model", 6, countLdapUsersInUserManager()); + 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"); + getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif")); ldap.sync(); assertEquals("Number of ldap groups in gitblit team model", 1, countLdapTeamsInUserManager()); } @Test + public void syncUpdateUsersAndGroupsAdminProperty() 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"); + ldap.sync(); + + UserModel user = userManager.getUserModel("UserOne"); + assertNotNull(user); + assertFalse(user.canAdmin); + assertTrue(user.canAdmin()); + + user = userManager.getUserModel("UserTwo"); + assertNotNull(user); + assertFalse(user.canAdmin); + assertTrue(user.canAdmin()); + + user = userManager.getUserModel("UserThree"); + assertNotNull(user); + assertTrue(user.canAdmin); + assertTrue(user.canAdmin()); + + user = userManager.getUserModel("UserFour"); + assertNotNull(user); + assertFalse(user.canAdmin); + assertFalse(user.canAdmin()); + + TeamModel team = userManager.getTeamModel("Git_Admins"); + assertNotNull(team); + assertTrue(team.canAdmin); + + team = userManager.getTeamModel("Git Admins"); + assertNotNull(team); + assertTrue(team.canAdmin); + + team = userManager.getTeamModel("Git_Users"); + assertNotNull(team); + assertFalse(team.canAdmin); + } + + @Test + public void syncNotUpdateUsersAndGroupsAdminProperty() throws Exception { + settings.put(Keys.realm.ldap.synchronize, "true"); + settings.put(Keys.realm.ldap.maintainTeams, "false"); + ldap.sync(); + + UserModel user = userManager.getUserModel("UserOne"); + assertNotNull(user); + assertTrue(user.canAdmin); + assertTrue(user.canAdmin()); + + user = userManager.getUserModel("UserTwo"); + assertNotNull(user); + assertFalse(user.canAdmin); + assertTrue(user.canAdmin()); + + user = userManager.getUserModel("UserThree"); + assertNotNull(user); + assertFalse(user.canAdmin); + assertFalse(user.canAdmin()); + + user = userManager.getUserModel("UserFour"); + assertNotNull(user); + assertFalse(user.canAdmin); + assertFalse(user.canAdmin()); + + TeamModel team = userManager.getTeamModel("Git_Admins"); + assertNotNull(team); + assertFalse(team.canAdmin); + + team = userManager.getTeamModel("Git Admins"); + assertNotNull(team); + assertTrue(team.canAdmin); + + team = userManager.getTeamModel("Git_Users"); + assertNotNull(team); + assertFalse(team.canAdmin); + } + + @Test public void testAuthenticationManager() { UserModel userOneModel = auth.authenticate("UserOne", "userOnePassword".toCharArray(), null); assertNotNull(userOneModel); assertNotNull(userOneModel.getTeam("git_admins")); assertNotNull(userOneModel.getTeam("git_users")); - assertTrue(userOneModel.canAdmin); UserModel userOneModelFailedAuth = auth.authenticate("UserOne", "userTwoPassword".toCharArray(), null); assertNull(userOneModelFailedAuth); @@ -254,18 +588,115 @@ assertNotNull(userTwoModel.getTeam("git_users")); assertNull(userTwoModel.getTeam("git_admins")); assertNotNull(userTwoModel.getTeam("git admins")); - assertTrue(userTwoModel.canAdmin); UserModel userThreeModel = auth.authenticate("UserThree", "userThreePassword".toCharArray(), null); assertNotNull(userThreeModel); assertNotNull(userThreeModel.getTeam("git_users")); assertNull(userThreeModel.getTeam("git_admins")); + + 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")); + } + + @Test + public void testAuthenticationManagerAdminPropertyTeamsInLdap() { + UserModel userOneModel = auth.authenticate("UserOne", "userOnePassword".toCharArray(), null); + assertNotNull(userOneModel); + assertNotNull(userOneModel.getTeam("git_admins")); + assertNull(userOneModel.getTeam("git admins")); + assertNotNull(userOneModel.getTeam("git_users")); + assertFalse(userOneModel.canAdmin); + assertTrue(userOneModel.canAdmin()); + assertTrue(userOneModel.getTeam("git_admins").canAdmin); + assertFalse(userOneModel.getTeam("git_users").canAdmin); + + UserModel userOneModelFailedAuth = auth.authenticate("UserOne", "userTwoPassword".toCharArray(), null); + assertNull(userOneModelFailedAuth); + + UserModel userTwoModel = auth.authenticate("UserTwo", "userTwoPassword".toCharArray(), null); + assertNotNull(userTwoModel); + assertNotNull(userTwoModel.getTeam("git_users")); + assertNull(userTwoModel.getTeam("git_admins")); + assertNotNull(userTwoModel.getTeam("git admins")); + assertFalse(userTwoModel.canAdmin); + assertTrue(userTwoModel.canAdmin()); + assertTrue(userTwoModel.getTeam("git admins").canAdmin); + assertFalse(userTwoModel.getTeam("git_users").canAdmin); + + UserModel userThreeModel = auth.authenticate("UserThree", "userThreePassword".toCharArray(), null); + assertNotNull(userThreeModel); + assertNotNull(userThreeModel.getTeam("git_users")); + assertNull(userThreeModel.getTeam("git_admins")); + assertNull(userThreeModel.getTeam("git admins")); assertTrue(userThreeModel.canAdmin); + assertTrue(userThreeModel.canAdmin()); + assertFalse(userThreeModel.getTeam("git_users").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); + assertFalse(userFourModel.canAdmin()); + assertFalse(userFourModel.getTeam("git_users").canAdmin); + } + + @Test + public void testAuthenticationManagerAdminPropertyTeamsNotInLdap() { + settings.put(Keys.realm.ldap.maintainTeams, "false"); + + UserModel userOneModel = auth.authenticate("UserOne", "userOnePassword".toCharArray(), null); + assertNotNull(userOneModel); + assertNotNull(userOneModel.getTeam("git_admins")); + assertNull(userOneModel.getTeam("git admins")); + assertNotNull(userOneModel.getTeam("git_users")); + assertTrue(userOneModel.canAdmin); + assertTrue(userOneModel.canAdmin()); + assertFalse(userOneModel.getTeam("git_admins").canAdmin); + assertFalse(userOneModel.getTeam("git_users").canAdmin); + + UserModel userOneModelFailedAuth = auth.authenticate("UserOne", "userTwoPassword".toCharArray(), null); + assertNull(userOneModelFailedAuth); + + UserModel userTwoModel = auth.authenticate("UserTwo", "userTwoPassword".toCharArray(), null); + assertNotNull(userTwoModel); + assertNotNull(userTwoModel.getTeam("git_users")); + assertNull(userTwoModel.getTeam("git_admins")); + assertNotNull(userTwoModel.getTeam("git admins")); + assertFalse(userTwoModel.canAdmin); + assertTrue(userTwoModel.canAdmin()); + assertTrue(userTwoModel.getTeam("git admins").canAdmin); + assertFalse(userTwoModel.getTeam("git_users").canAdmin); + + UserModel userThreeModel = auth.authenticate("UserThree", "userThreePassword".toCharArray(), null); + assertNotNull(userThreeModel); + assertNotNull(userThreeModel.getTeam("git_users")); + assertNull(userThreeModel.getTeam("git_admins")); + assertNull(userThreeModel.getTeam("git admins")); + assertFalse(userThreeModel.canAdmin); + assertFalse(userThreeModel.canAdmin()); + assertFalse(userThreeModel.getTeam("git_users").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); + assertFalse(userFourModel.canAdmin()); + assertFalse(userFourModel.getTeam("git_users").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 +707,12 @@ assertNull(userOneModelFailedAuth); } + + private InMemoryDirectoryServer getDS() + { + return authMode.getDS(); + } + private int countLdapUsersInUserManager() { int ldapAccountCount = 0; for (UserModel userModel : userManager.getAllUsers()) { @@ -296,4 +733,120 @@ 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; + } + } + }
diff --git a/src/test/java/com/gitblit/tests/MarkdownUtilsTest.java b/src/test/java/com/gitblit/tests/MarkdownUtilsTest.java index e40f105..bc7aad4 100644 --- a/src/test/java/com/gitblit/tests/MarkdownUtilsTest.java +++ b/src/test/java/com/gitblit/tests/MarkdownUtilsTest.java
@@ -15,8 +15,14 @@ */ package com.gitblit.tests; +import java.util.HashMap; +import java.util.Map; + import org.junit.Test; +import com.gitblit.IStoredSettings; +import com.gitblit.Keys; +import com.gitblit.tests.mock.MemorySettings; import com.gitblit.utils.MarkdownUtils; public class MarkdownUtilsTest extends GitblitUnitTest { @@ -39,4 +45,70 @@ assertEquals("<table><tr><td><test></td></tr></table>", MarkdownUtils.transformMarkdown("<table><tr><td><test></td></tr></table>")); } -} \ No newline at end of file + + + @Test + public void testUserMentions() { + IStoredSettings settings = getSettings(); + String repositoryName = "test3"; + String mentionHtml = "<strong><a href=\"http://localhost/user/%1$s\">@%1$s</a></strong>"; + + String input = "@j.doe"; + String output = "<p>" + String.format(mentionHtml, "j.doe") + "</p>"; + assertEquals(output, MarkdownUtils.transformGFM(settings, input, repositoryName)); + + input = " @j.doe"; + output = "<p>" + String.format(mentionHtml, "j.doe") + "</p>"; + assertEquals(output, MarkdownUtils.transformGFM(settings, input, repositoryName)); + + input = "@j.doe."; + output = "<p>" + String.format(mentionHtml, "j.doe") + ".</p>"; + assertEquals(output, MarkdownUtils.transformGFM(settings, input, repositoryName)); + + input = "To @j.doe: ask @jim.beam!"; + output = "<p>To " + String.format(mentionHtml, "j.doe") + + ": ask " + String.format(mentionHtml, "jim.beam") + "!</p>"; + assertEquals(output, MarkdownUtils.transformGFM(settings, input, repositoryName)); + + input = "@sta.rt\n" + + "\n" + + "User mentions in tickets are broken.\n" + + "So:\n" + + "@mc_guyver can fix this.\n" + + "@j.doe, can you test after the fix by @m+guyver?\n" + + "Please review this, @jim.beam!\n" + + "Was reported by @jill and @j!doe from jane@doe yesterday.\n" + + "\n" + + "@jack.daniels can vote for john@wayne.name hopefully.\n" + + "@en.de"; + output = "<p>" + String.format(mentionHtml, "sta.rt") + "</p>" + + "<p>" + "User mentions in tickets are broken.<br/>" + + "So:<br/>" + + String.format(mentionHtml, "mc_guyver") + " can fix this.<br/>" + + String.format(mentionHtml, "j.doe") + ", can you test after the fix by " + String.format(mentionHtml, "m+guyver") + "?<br/>" + + "Please review this, " + String.format(mentionHtml, "jim.beam") + "!<br/>" + + "Was reported by " + String.format(mentionHtml, "jill") + + " and " + String.format(mentionHtml, "j!doe") + + " from <a href=\"mailto:jane@doe\">jane@doe</a> yesterday." + + "</p>" + + "<p>" + String.format(mentionHtml, "jack.daniels") + " can vote for " + + "<a href=\"mailto:john@wayne.name\">john@wayne.name</a> hopefully.<br/>" + + String.format(mentionHtml, "en.de") + + "</p>"; + assertEquals(output, MarkdownUtils.transformGFM(settings, input, repositoryName)); + + } + + + + + private MemorySettings getSettings() { + Map<String, Object> backingMap = new HashMap<String, Object>(); + + backingMap.put(Keys.web.canonicalUrl, "http://localhost"); + backingMap.put(Keys.web.shortCommitIdLength, "7"); + + MemorySettings ms = new MemorySettings(backingMap); + return ms; + } +}
diff --git a/src/test/resources/ldap/users.conf b/src/test/resources/ldap/users.conf index 7d1e319..a2390fa 100644 --- a/src/test/resources/ldap/users.conf +++ b/src/test/resources/ldap/users.conf
@@ -10,7 +10,7 @@ displayName = Mrs. User Three emailAddress = userthree@gitblit.com accountType = LDAP - role = "#admin" + role = "#none" [user "userfive"] password = "#externalAccount" cookie = 220bafef069b8b399b2597644015b6b0f4667982 @@ -31,7 +31,7 @@ displayName = Mr. User Two emailAddress = usertwo@gitblit.com accountType = LDAP - role = "#admin" + role = "#none" [user "basic"] password = MD5:f17aaabc20bfe045075927934fed52d2 cookie = dd94709528bb1c83d08f3088d4043f4742891f4f @@ -63,6 +63,6 @@ user = userthree user = userfour [team "Git Admins"] - role = "#none" + role = "#admin" accountType = LOCAL user = usertwo