- Redirect to summary page on edit repository (issue-405) | - Redirect to summary page on edit repository (issue-405) | ||||
- Option to allow LDAP users to directly authenticate without performing LDAP searches (pr-162) | - Option to allow LDAP users to directly authenticate without performing LDAP searches (pr-162) | ||||
- Replace JCommander with args4j to be consistent with other tools (ticket-28) | - Replace JCommander with args4j to be consistent with other tools (ticket-28) | ||||
- Sort repository urls by descending permissions and by transport security within equal permissions | |||||
additions: | additions: | ||||
- Added an SSH daemon with public key authentication (issue-369, ticket-6) | - Added an SSH daemon with public key authentication (issue-369, ticket-6) | ||||
- Added beginnings of a plugin framework for extending Gitblit (issue-381, ticket-23) | - Added beginnings of a plugin framework for extending Gitblit (issue-381, ticket-23) | ||||
- Added a French translation (pr-163) | - Added a French translation (pr-163) | ||||
- Added a setting to control what transports may be used for pushes | |||||
dependencyChanges: | dependencyChanges: | ||||
- args4j 2.0.26 | - args4j 2.0.26 | ||||
- JGit 3.3.1 | - JGit 3.3.1 | ||||
settings: | settings: | ||||
- { name: 'realm.ldap.bindpattern', defaultValue: ' ' } | - { name: 'realm.ldap.bindpattern', defaultValue: ' ' } | ||||
- { name: 'tickets.closeOnPushCommitMessageRegex', defaultValue: '(?:fixes|closes)[\\s-]+#?(\\d+)' } | - { name: 'tickets.closeOnPushCommitMessageRegex', defaultValue: '(?:fixes|closes)[\\s-]+#?(\\d+)' } | ||||
- { name: 'git.acceptedPushTransports', defaultValue: ' ' } | |||||
- { name: 'git.sshPort', defaultValue: '29418' } | - { name: 'git.sshPort', defaultValue: '29418' } | ||||
- { name: 'git.sshBindInterface', defaultValue: ' ' } | - { name: 'git.sshBindInterface', defaultValue: ' ' } | ||||
- { name: 'git.sshKeysManager', defaultValue: 'com.gitblit.transport.ssh.FileKeyManager' } | - { name: 'git.sshKeysManager', defaultValue: 'com.gitblit.transport.ssh.FileKeyManager' } |
# SINCE 0.9.0 | # SINCE 0.9.0 | ||||
git.onlyAccessBareRepositories = false | git.onlyAccessBareRepositories = false | ||||
# Specify the list of acceptable transports for pushes. | |||||
# If this setting is empty, all transports are acceptable. | |||||
# | |||||
# Valid choices are: GIT HTTP HTTPS SSH | |||||
# | |||||
# SINCE 1.5.0 | |||||
# SPACE-DELIMITED | |||||
git.acceptedPushTransports = HTTP HTTPS SSH | |||||
# Allow an authenticated user to create a destination repository on a push if | # Allow an authenticated user to create a destination repository on a push if | ||||
# the repository does not already exist. | # the repository does not already exist. | ||||
# | # |
} | } | ||||
} | } | ||||
public static enum Transport { | |||||
// ordered for url advertisements, assuming equal access permissions | |||||
SSH, HTTPS, HTTP, GIT; | |||||
public static Transport fromString(String value) { | |||||
for (Transport t : values()) { | |||||
if (t.name().equalsIgnoreCase(value)) { | |||||
return t; | |||||
} | |||||
} | |||||
return null; | |||||
} | |||||
public static Transport fromUrl(String url) { | |||||
String scheme = url.substring(0, url.indexOf("://")); | |||||
return fromString(scheme); | |||||
} | |||||
} | |||||
@Documented | @Documented | ||||
@Retention(RetentionPolicy.RUNTIME) | @Retention(RetentionPolicy.RUNTIME) | ||||
public @interface Unused { | public @interface Unused { |
import java.text.MessageFormat; | import java.text.MessageFormat; | ||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Collections; | |||||
import java.util.Comparator; | |||||
import java.util.HashSet; | |||||
import java.util.List; | import java.util.List; | ||||
import java.util.Set; | |||||
import javax.inject.Singleton; | import javax.inject.Singleton; | ||||
import javax.servlet.http.HttpServletRequest; | import javax.servlet.http.HttpServletRequest; | ||||
import com.gitblit.Constants.AccessPermission; | import com.gitblit.Constants.AccessPermission; | ||||
import com.gitblit.Constants.Transport; | |||||
import com.gitblit.manager.GitblitManager; | import com.gitblit.manager.GitblitManager; | ||||
import com.gitblit.manager.IAuthenticationManager; | import com.gitblit.manager.IAuthenticationManager; | ||||
import com.gitblit.manager.IFederationManager; | import com.gitblit.manager.IFederationManager; | ||||
return new Object [] { new GitBlitModule()}; | return new Object [] { new GitBlitModule()}; | ||||
} | } | ||||
protected boolean acceptPush(Transport byTransport) { | |||||
if (byTransport == null) { | |||||
logger.info("Unknown transport, push rejected!"); | |||||
return false; | |||||
} | |||||
Set<Transport> transports = new HashSet<Transport>(); | |||||
for (String value : getSettings().getStrings(Keys.git.acceptedPushTransports)) { | |||||
Transport transport = Transport.fromString(value); | |||||
if (transport == null) { | |||||
logger.info(String.format("Ignoring unknown registered transport %s", value)); | |||||
continue; | |||||
} | |||||
transports.add(transport); | |||||
} | |||||
if (transports.isEmpty()) { | |||||
// no transports are explicitly specified, all are acceptable | |||||
return true; | |||||
} | |||||
// verify that the transport is permitted | |||||
return transports.contains(byTransport); | |||||
} | |||||
/** | /** | ||||
* Returns a list of repository URLs and the user access permission. | * Returns a list of repository URLs and the user access permission. | ||||
* | * | ||||
if (settings.getBoolean(Keys.git.enableGitServlet, true)) { | if (settings.getBoolean(Keys.git.enableGitServlet, true)) { | ||||
AccessPermission permission = user.getRepositoryPermission(repository).permission; | AccessPermission permission = user.getRepositoryPermission(repository).permission; | ||||
if (permission.exceeds(AccessPermission.NONE)) { | if (permission.exceeds(AccessPermission.NONE)) { | ||||
Transport transport = Transport.fromString(request.getScheme()); | |||||
if (permission.atLeast(AccessPermission.PUSH) && !acceptPush(transport)) { | |||||
// downgrade the repo permission for this transport | |||||
// because it is not an acceptable PUSH transport | |||||
permission = AccessPermission.CLONE; | |||||
} | |||||
list.add(new RepositoryUrl(getRepositoryUrl(request, username, repository), permission)); | list.add(new RepositoryUrl(getRepositoryUrl(request, username, repository), permission)); | ||||
} | } | ||||
} | } | ||||
if (!StringUtils.isEmpty(sshDaemonUrl)) { | if (!StringUtils.isEmpty(sshDaemonUrl)) { | ||||
AccessPermission permission = user.getRepositoryPermission(repository).permission; | AccessPermission permission = user.getRepositoryPermission(repository).permission; | ||||
if (permission.exceeds(AccessPermission.NONE)) { | if (permission.exceeds(AccessPermission.NONE)) { | ||||
if (permission.atLeast(AccessPermission.PUSH) && !acceptPush(Transport.SSH)) { | |||||
// downgrade the repo permission for this transport | |||||
// because it is not an acceptable PUSH transport | |||||
permission = AccessPermission.CLONE; | |||||
} | |||||
list.add(new RepositoryUrl(sshDaemonUrl, permission)); | list.add(new RepositoryUrl(sshDaemonUrl, permission)); | ||||
} | } | ||||
} | } | ||||
if (!StringUtils.isEmpty(gitDaemonUrl)) { | if (!StringUtils.isEmpty(gitDaemonUrl)) { | ||||
AccessPermission permission = servicesManager.getGitDaemonAccessPermission(user, repository); | AccessPermission permission = servicesManager.getGitDaemonAccessPermission(user, repository); | ||||
if (permission.exceeds(AccessPermission.NONE)) { | if (permission.exceeds(AccessPermission.NONE)) { | ||||
if (permission.atLeast(AccessPermission.PUSH) && !acceptPush(Transport.GIT)) { | |||||
// downgrade the repo permission for this transport | |||||
// because it is not an acceptable PUSH transport | |||||
permission = AccessPermission.CLONE; | |||||
} | |||||
list.add(new RepositoryUrl(gitDaemonUrl, permission)); | list.add(new RepositoryUrl(gitDaemonUrl, permission)); | ||||
} | } | ||||
} | } | ||||
list.add(new RepositoryUrl(MessageFormat.format(url, repository.name), null)); | list.add(new RepositoryUrl(MessageFormat.format(url, repository.name), null)); | ||||
} | } | ||||
} | } | ||||
// sort transports by highest permission and then by transport security | |||||
Collections.sort(list, new Comparator<RepositoryUrl>() { | |||||
@Override | |||||
public int compare(RepositoryUrl o1, RepositoryUrl o2) { | |||||
if (!o1.isExternal() && o2.isExternal()) { | |||||
// prefer Gitblit over external | |||||
return -1; | |||||
} else if (o1.isExternal() && !o2.isExternal()) { | |||||
// prefer Gitblit over external | |||||
return 1; | |||||
} else if (o1.isExternal() && o2.isExternal()) { | |||||
// sort by Transport ordinal | |||||
return o1.transport.compareTo(o2.transport); | |||||
} else if (o1.permission.exceeds(o2.permission)) { | |||||
// prefer highest permission | |||||
return -1; | |||||
} else if (o2.permission.exceeds(o1.permission)) { | |||||
// prefer highest permission | |||||
return 1; | |||||
} | |||||
// prefer more secure transports | |||||
return o1.transport.compareTo(o2.transport); | |||||
} | |||||
}); | |||||
return list; | return list; | ||||
} | } | ||||
*/ | */ | ||||
package com.gitblit.git; | package com.gitblit.git; | ||||
import java.util.HashSet; | |||||
import java.util.Set; | |||||
import javax.servlet.http.HttpServletRequest; | import javax.servlet.http.HttpServletRequest; | ||||
import org.eclipse.jgit.lib.PersonIdent; | import org.eclipse.jgit.lib.PersonIdent; | ||||
import org.slf4j.Logger; | import org.slf4j.Logger; | ||||
import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||
import com.gitblit.Constants.Transport; | |||||
import com.gitblit.IStoredSettings; | import com.gitblit.IStoredSettings; | ||||
import com.gitblit.Keys; | import com.gitblit.Keys; | ||||
import com.gitblit.manager.IGitblit; | import com.gitblit.manager.IGitblit; | ||||
String origin = ""; | String origin = ""; | ||||
String gitblitUrl = ""; | String gitblitUrl = ""; | ||||
int timeout = 0; | int timeout = 0; | ||||
Transport transport = null; | |||||
if (req instanceof HttpServletRequest) { | if (req instanceof HttpServletRequest) { | ||||
// http/https request may or may not be authenticated | // http/https request may or may not be authenticated | ||||
user = u; | user = u; | ||||
} | } | ||||
} | } | ||||
// determine the transport | |||||
if ("http".equals(client.getScheme())) { | |||||
transport = Transport.HTTP; | |||||
} else if ("https".equals(client.getScheme())) { | |||||
transport = Transport.HTTPS; | |||||
} | |||||
} else if (req instanceof GitDaemonClient) { | } else if (req instanceof GitDaemonClient) { | ||||
// git daemon request is always anonymous | // git daemon request is always anonymous | ||||
GitDaemonClient client = (GitDaemonClient) req; | GitDaemonClient client = (GitDaemonClient) req; | ||||
// set timeout from Git daemon | // set timeout from Git daemon | ||||
timeout = client.getDaemon().getTimeout(); | timeout = client.getDaemon().getTimeout(); | ||||
transport = Transport.GIT; | |||||
} else if (req instanceof SshDaemonClient) { | } else if (req instanceof SshDaemonClient) { | ||||
// SSH request is always authenticated | // SSH request is always authenticated | ||||
SshDaemonClient client = (SshDaemonClient) req; | SshDaemonClient client = (SshDaemonClient) req; | ||||
repositoryName = client.getRepositoryName(); | repositoryName = client.getRepositoryName(); | ||||
origin = client.getRemoteAddress().toString(); | origin = client.getRemoteAddress().toString(); | ||||
user = client.getUser(); | user = client.getUser(); | ||||
transport = Transport.SSH; | |||||
} | |||||
if (!acceptPush(transport)) { | |||||
throw new ServiceNotAuthorizedException(); | |||||
} | } | ||||
boolean allowAnonymousPushes = settings.getBoolean(Keys.git.allowAnonymousPushes, false); | boolean allowAnonymousPushes = settings.getBoolean(Keys.git.allowAnonymousPushes, false); | ||||
return rp; | return rp; | ||||
} | } | ||||
protected boolean acceptPush(Transport byTransport) { | |||||
if (byTransport == null) { | |||||
logger.info("Unknown transport, push rejected!"); | |||||
return false; | |||||
} | |||||
Set<Transport> transports = new HashSet<Transport>(); | |||||
for (String value : gitblit.getSettings().getStrings(Keys.git.acceptedPushTransports)) { | |||||
Transport transport = Transport.fromString(value); | |||||
if (transport == null) { | |||||
logger.info(String.format("Ignoring unknown registered transport %s", value)); | |||||
continue; | |||||
} | |||||
transports.add(transport); | |||||
} | |||||
if (transports.isEmpty()) { | |||||
// no transports are explicitly specified, all are acceptable | |||||
return true; | |||||
} | |||||
// verify that the transport is permitted | |||||
return transports.contains(byTransport); | |||||
} | |||||
} | } |
import java.io.Serializable; | import java.io.Serializable; | ||||
import com.gitblit.Constants.AccessPermission; | import com.gitblit.Constants.AccessPermission; | ||||
import com.gitblit.Constants.Transport; | |||||
/** | /** | ||||
* Represents a git repository url and it's associated access permission for the | * Represents a git repository url and it's associated access permission for the | ||||
private static final long serialVersionUID = 1L; | private static final long serialVersionUID = 1L; | ||||
public final Transport transport; | |||||
public final String url; | public final String url; | ||||
public final AccessPermission permission; | public final AccessPermission permission; | ||||
public RepositoryUrl(String url, AccessPermission permission) { | public RepositoryUrl(String url, AccessPermission permission) { | ||||
this.transport = Transport.fromUrl(url); | |||||
this.url = url; | this.url = url; | ||||
this.permission = permission; | this.permission = permission; | ||||
} | } |