merge with branch 1.x

This commit is contained in:
Sebastian Sdorra
2017-06-25 19:01:33 +02:00
81 changed files with 4617 additions and 1319 deletions

View File

@@ -28,3 +28,4 @@ Desktop DF$
\.idea$
# jrebel
rebel.xml
\.pyc

View File

@@ -259,7 +259,7 @@
<author>true</author>
<keywords>true</keywords>
<links>
<link>http://download.oracle.com/javase/6/docs/api/</link>
<link>http://download.oracle.com/javase/8/docs/api/</link>
<link>http://download.oracle.com/docs/cd/E17802_01/products/products/servlet/2.5/docs/servlet-2_5-mr2/</link>
<link>http://jersey.java.net/nonav/apidocs/${jersey.version}/jersey/</link>
<link>https://google.github.io/guice/api-docs/${guice.version}/javadoc</link>
@@ -483,7 +483,7 @@
<logback.version>1.1.10</logback.version>
<servlet.version>3.0.1</servlet.version>
<guice.version>4.0</guice.version>
<jersey.version>1.19.3</jersey.version>
<jersey.version>1.19.4</jersey.version>
<!-- event bus -->
<legman.version>1.2.0</legman.version>
@@ -497,8 +497,8 @@
<shiro.version>1.4.0-RC2</shiro.version>
<!-- repostitory libraries -->
<jgit.version>v4.5.0.201609210915-r-scm1</jgit.version>
<svnkit.version>1.8.14-scm1</svnkit.version>
<jgit.version>v4.5.2.201704071617-r-scm1</jgit.version>
<svnkit.version>1.8.15-scm1</svnkit.version>
<!-- util libraries -->
<guava.version>16.0.1</guava.version>

View File

@@ -123,7 +123,7 @@ public class AdvancedHttpRequestWithBody
}
/**
* Transforms the given object to a xml string and set this string as request
* Transforms the given object to a json string and set this string as request
* content.
*
* @param object object to transform

View File

@@ -253,7 +253,9 @@ public abstract class AdvancedHttpResponse
}
/**
* Transforms the response content from xml to the given type.
* Transforms the response content to the given type. The method will use
* the {@link ContentTransformer} which is responsible for the the given
* content type.
*
* @param <T> object type
* @param type object type

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.repository;
import sonia.scm.plugin.ExtensionPoint;
/**
* ExtensionPoint to modify the path matching behaviour for a certain type of repositories.
*
* @author Sebastian Sdorra
* @since 1.54
*/
@ExtensionPoint
public interface RepositoryPathMatcher {
/**
* Returns {@code true} if the path matches the repository.
*
* @param repository repository
* @param path requested path without context and without type information extracted from uri
*
* @return {@code true} if the path matches
*/
boolean isPathMatching(Repository repository, String path);
/**
* Returns the type of repository for which the matcher is responsible.
*
* @return type of repository
*/
String getType();
}

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) 2014, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.security;
import sonia.scm.event.Event;
/**
* This type of event is fired whenever a authorization relevant data changes. This event
* is especially useful for cache invalidation.
*
* @author Sebastian Sdorra
* @since 1.52
*/
@Event
public final class AuthorizationChangedEvent {
private final String nameOfAffectedUser;
private AuthorizationChangedEvent(String nameOfAffectedUser) {
this.nameOfAffectedUser = nameOfAffectedUser;
}
/**
* Returns {@code true} if every user is affected by this data change.
*
* @return {@code true} if every user is affected
*/
public boolean isEveryUserAffected(){
return nameOfAffectedUser != null;
}
/**
* Returns the name of the user which is affected by this event.
*
* @return name of affected user
*/
public String getNameOfAffectedUser(){
return nameOfAffectedUser;
}
/**
* Creates a new event which affects every user.
*
* @return new event for every user
*/
public static AuthorizationChangedEvent createForEveryUser() {
return new AuthorizationChangedEvent(null);
}
/**
* Create a new event which affect a single user.
*
* @param nameOfAffectedUser name of affected user
*
* @return new event for a single user
*/
public static AuthorizationChangedEvent createForUser(String nameOfAffectedUser) {
return new AuthorizationChangedEvent(nameOfAffectedUser);
}
}

View File

@@ -85,4 +85,13 @@ public interface Blob
* @throws IOException
*/
public OutputStream getOutputStream() throws IOException;
/**
*
* Returns the size (in bytes) of the blob.
* @since 1.54
*/
public long getSize();
}

View File

@@ -58,6 +58,17 @@ public class I18nMessagesTest
@Test
public void testI18n()
{
/*
lookup-order for this test:
- TM_en (es specified, but not ava)
- TM_<execution-locale>
- TM
This means that, if there is no default locale specified, this test accidentally passes on non-german machines, an fails on german machines, since the execution locale is de_DE, which is checked even before the fallback locale is considered.
*/
Locale.setDefault(Locale.ENGLISH);
TestMessages msg = I18nMessages.get(TestMessages.class);
assertEquals("Normal Key", msg.normalKey);

View File

@@ -78,6 +78,12 @@ public class ValidationUtilTest
assertTrue(ValidationUtil.isMailAddressValid("sdorra@ostfalia.de"));
assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@hbk-bs.de"));
assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@gmail.com"));
assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@t.co"));
assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@ucla.college"));
assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@example.xn--p1ai"));
// issue 909
assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@scm.solutions"));
// false
assertFalse(ValidationUtil.isMailAddressValid("ostfalia.de"));

View File

@@ -75,4 +75,13 @@ public final class FileBlob implements Blob {
return new FileOutputStream(file);
}
@Override
public long getSize() {
if (this.file.isFile()) {
return this.file.length();
} else {
//to sum up all other cases, in which we cannot determine a size
return -1;
}
}
}

165
scm-plugin-backend/pom.xml Normal file
View File

@@ -0,0 +1,165 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>scm</artifactId>
<groupId>sonia.scm</groupId>
<version>1.55-SNAPSHOT</version>
</parent>
<groupId>sonia.scm</groupId>
<artifactId>scm-plugin-backend</artifactId>
<packaging>war</packaging>
<version>1.55-SNAPSHOT</version>
<name>${project.artifactId}</name>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>${servlet.version}</version>
<scope>provided</scope>
</dependency>
<!-- fix javadoc -->
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<!-- logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>${freemarker.version}</version>
</dependency>
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-core</artifactId>
<version>1.55-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.sun.jersey.contribs</groupId>
<artifactId>jersey-guice</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>${ehcache.version}</version>
</dependency>
<dependency>
<groupId>org.imgscalr</groupId>
<artifactId>imgscalr-lib</artifactId>
<version>4.2</version>
</dependency>
<!-- security -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-guice</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
</dependencies>
<properties>
<sonar.exclusions>src/main/webapp/template/**</sonar.exclusions>
</properties>
<build>
<plugins>
<plugin>
<groupId>com.mycila.maven-license-plugin</groupId>
<artifactId>maven-license-plugin</artifactId>
<version>1.9.0</version>
<configuration>
<header>http://download.scm-manager.org/licenses/mvn-license.txt</header>
<includes>
<include>src/**</include>
<include>**/test/**</include>
</includes>
<excludes>
<exclude>target/**</exclude>
<exclude>.hg/**</exclude>
<exclude>**/html5.js</exclude>
<exclude>**/*.html</exclude>
<exclude>**/fancybox/**</exclude>
</excludes>
<strictCheck>true</strictCheck>
</configuration>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>${jetty.version}</version>
<configuration>
<stopPort>8004</stopPort>
<stopKey>STOP</stopKey>
<webApp>
<contextPath>/scm-plugin-backend</contextPath>
</webApp>
<source>${project.build.javaLevel}</source>
<target>${project.build.javaLevel}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<scanIntervalSeconds>0</scanIntervalSeconds>
</configuration>
</plugin>
</plugins>
<finalName>scm-plugin-backend</finalName>
</build>
</project>

View File

@@ -0,0 +1,231 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.plugin;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Objects;
import org.apache.shiro.authc.SaltedAuthenticationInfo;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.ByteSource;
//~--- JDK imports ------------------------------------------------------------
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
/**
*
* @author Sebastian Sdorra
*/
@XmlRootElement(name = "admin-account")
@XmlAccessorType(XmlAccessType.FIELD)
public class AdminAccountConfiguration implements SaltedAuthenticationInfo
{
/** Field description */
private static final long serialVersionUID = -8678832281151044462L;
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*/
public AdminAccountConfiguration() {}
/**
* Constructs ...
*
*
* @param username
* @param salt
* @param password
*/
public AdminAccountConfiguration(String username, String salt,
String password)
{
this.username = username;
this.salt = salt;
this.password = password;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param obj
*
* @return
*/
@Override
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
final AdminAccountConfiguration other = (AdminAccountConfiguration) obj;
return Objects.equal(username, other.username)
&& Objects.equal(salt, other.salt)
&& Objects.equal(password, other.password);
}
/**
* Method description
*
*
* @return
*/
@Override
public int hashCode()
{
return Objects.hashCode(username, salt, password);
}
/**
* Method description
*
*
* @return
*/
@Override
@SuppressWarnings("squid:S2068")
public String toString()
{
//J-
return Objects.toStringHelper(this)
.add("username", username)
.add("salt", "xxx")
.add("password", "xxx")
.toString();
//J+
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@Override
public Object getCredentials()
{
return password;
}
/**
* Method description
*
*
* @return
*/
@Override
public ByteSource getCredentialsSalt()
{
return ByteSource.Util.bytes(Base64.decode(salt));
}
/**
* Method description
*
*
* @return
*/
public String getPassword()
{
return password;
}
/**
* Method description
*
*
* @return
*/
@Override
public PrincipalCollection getPrincipals()
{
// TODO
return new SimplePrincipalCollection(username, "scm-backend");
}
/**
* Method description
*
*
* @return
*/
public String getSalt()
{
return salt;
}
/**
* Method description
*
*
* @return
*/
public String getUsername()
{
return username;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String password;
/** Field description */
private String salt;
/** Field description */
private String username;
}

View File

@@ -0,0 +1,204 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.plugin.security;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.name.Named;
import com.google.inject.name.Names;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.guice.web.ShiroWebModule;
import org.apache.shiro.util.ByteSource;
import sonia.scm.plugin.Roles;
//~--- JDK imports ------------------------------------------------------------
import javax.servlet.ServletContext;
import javax.swing.JOptionPane;
/**
*
* @author Sebastian Sdorra
*/
public class SecurityModule extends ShiroWebModule
{
/** Field description */
private static final String ATTRIBUTE_FAILURE = "shiroLoginFailure";
/** Field description */
private static final String HASH_ALGORITHM = "SHA-256";
/** Field description */
private static final int HASH_ITERATIONS = 1024;
/** Field description */
private static final String PAGE_LOGIN = "/page/login.html";
/** Field description */
private static final String PAGE_SUCCESS = "/admin/index.html";
/** Field description */
private static final String PAGE_UNAUTHORIZED = "/error/unauthorized.html";
/** Field description */
@SuppressWarnings("squid:S2068")
private static final String PARAM_PASSWORD = "password";
/** Field description */
private static final String PARAM_REMEMBERME = "rememberme";
/** Field description */
private static final String PARAM_USERNAME = "username";
/** Field description */
private static final String PATTERN_ADMIN = "/admin/**";
/** Field description */
private static final Named NAMED_USERNAMEPARAM =
Names.named("shiro.usernameParam");
/** Field description */
private static final Named NAMED_UNAUTHORIZEDURL =
Names.named("shiro.unauthorizedUrl");
/** Field description */
private static final Named NAMED_SUCCESSURL = Names.named("shiro.successUrl");
/** Field description */
private static final Named NAMED_REMEMBERMEPARAM =
Names.named("shiro.rememberMeParam");
/** Field description */
private static final Named NAMED_PASSWORDPARAM =
Names.named("shiro.passwordParam");
/** Field description */
private static final Named NAMED_LOGINURL = Names.named("shiro.loginUrl");
/** Field description */
private static final Named NAMED_FAILUREKEYATTRIBUTE =
Names.named("shiro.failureKeyAttribute");
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param servletContext
*/
public SecurityModule(ServletContext servletContext)
{
super(servletContext);
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param args
*/
public static void main(String[] args)
{
String value = JOptionPane.showInputDialog("Password");
RandomNumberGenerator rng = new SecureRandomNumberGenerator();
ByteSource salt = rng.nextBytes();
SimpleHash hash = new SimpleHash(HASH_ALGORITHM, value, salt,
HASH_ITERATIONS);
System.out.append("Salt: ").println(salt.toBase64());
System.out.append("Hash: ").println(hash.toBase64());
}
/**
* Method description
*
*/
@Override
protected void configureShiroWeb()
{
bindConstants();
bindCredentialsMatcher();
// bind cache manager
bind(CacheManager.class).toProvider(CacheManagerProvider.class);
// bind realm
bindRealm().to(DefaultAdminRealm.class);
// add filters
addFilterChain(PAGE_LOGIN, AUTHC);
addFilterChain(PATTERN_ADMIN, AUTHC, config(ROLES, Roles.ADMIN));
}
/**
* Method description
*
*/
private void bindConstants()
{
bindConstant().annotatedWith(NAMED_LOGINURL).to(PAGE_LOGIN);
bindConstant().annotatedWith(NAMED_USERNAMEPARAM).to(PARAM_USERNAME);
bindConstant().annotatedWith(NAMED_PASSWORDPARAM).to(PARAM_PASSWORD);
bindConstant().annotatedWith(NAMED_REMEMBERMEPARAM).to(PARAM_REMEMBERME);
bindConstant().annotatedWith(NAMED_SUCCESSURL).to(PAGE_SUCCESS);
bindConstant().annotatedWith(NAMED_UNAUTHORIZEDURL).to(PAGE_UNAUTHORIZED);
bindConstant().annotatedWith(NAMED_FAILUREKEYATTRIBUTE).to(
ATTRIBUTE_FAILURE);
}
/**
* Method description
*
*/
private void bindCredentialsMatcher()
{
HashedCredentialsMatcher matcher =
new HashedCredentialsMatcher(HASH_ALGORITHM);
matcher.setHashIterations(HASH_ITERATIONS);
matcher.setStoredCredentialsHexEncoded(false);
bind(CredentialsMatcher.class).toInstance(matcher);
}
}

View File

@@ -38,6 +38,12 @@
<version>${jgit.version}</version>
</dependency>
<dependency>
<groupId>sonia.jgit</groupId>
<artifactId>org.eclipse.jgit.lfs.server</artifactId>
<version>${jgit.version}</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
@@ -80,11 +86,6 @@
<repositories>
<repository>
<id>jgit-repository</id>
<url>http://download.eclipse.org/jgit/maven</url>
</repository>
<repository>
<id>maven.scm-manager.org</id>
<name>scm-manager release repository</name>

View File

@@ -81,7 +81,10 @@ public class GitRepositoryHandler
/** Field description */
public static final String TYPE_NAME = "git";
public static final String DOT_GIT = ".git";
private static final Logger logger = LoggerFactory.getLogger(GitRepositoryHandler.class);
/** Field description */

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.repository;
import sonia.scm.plugin.Extension;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
/**
* Matches git repositories with ".git" and without ".git".
*
* @author Sebastian Sdorra
* @since 1.54
*/
@Extension
public class GitRepositoryPathMatcher implements RepositoryPathMatcher {
@Override
public boolean isPathMatching(Repository repository, String path) {
String repositoryName = repository.getName();
if (path.startsWith(repositoryName)) {
String pathPart = path.substring(repositoryName.length());
// git repository may also be named <<repo-name>>.git by convention
if (pathPart.startsWith(GitRepositoryHandler.DOT_GIT)) {
// if this is the case, just also cut it away
pathPart = pathPart.substring(GitRepositoryHandler.DOT_GIT.length());
}
return Util.isEmpty(pathPart) || pathPart.startsWith(HttpUtil.SEPARATOR_PATH);
}
return false;
}
@Override
public String getType() {
return GitRepositoryHandler.TYPE_NAME;
}
}

View File

@@ -70,6 +70,7 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import sonia.scm.web.GitUserAgentProvider;
/**
*
@@ -77,6 +78,8 @@ import javax.servlet.http.HttpServletRequest;
*/
public final class GitUtil
{
private static final GitUserAgentProvider GIT_USER_AGENT_PROVIDER = new GitUserAgentProvider();
/** Field description */
public static final String REF_HEAD = "HEAD";
@@ -696,7 +699,7 @@ public final class GitUtil
*/
public static boolean isGitClient(HttpServletRequest request)
{
return HttpUtil.userAgentStartsWith(request, USERAGENT_GIT);
return GIT_USER_AGENT_PROVIDER.parseUserAgent(request.getHeader(HttpUtil.HEADER_USERAGENT)) != null;
}
/**

View File

@@ -35,6 +35,7 @@ package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -57,7 +58,8 @@ import sonia.scm.filter.Filters;
import sonia.scm.filter.WebElement;
/**
*
* GitPermissionFilter decides if a git request requires write or read privileges.
*
* @author Sebastian Sdorra
*/
@Priority(Filters.PRIORITY_AUTHORIZATION)
@@ -65,79 +67,60 @@ import sonia.scm.filter.WebElement;
public class GitPermissionFilter extends ProviderPermissionFilter
{
/** Field description */
public static final String PARAMETER_SERVICE = "service";
private static final String PARAMETER_SERVICE = "service";
/** Field description */
public static final String PARAMETER_VALUE_RECEIVE = "git-receive-pack";
private static final String PARAMETER_VALUE_RECEIVE = "git-receive-pack";
/** Field description */
public static final String URI_RECEIVE_PACK = "git-receive-pack";
private static final String URI_RECEIVE_PACK = "git-receive-pack";
/** Field description */
public static final String URI_REF_INFO = "/info/refs";
private static final String URI_REF_INFO = "/info/refs";
private static final String METHOD_LFS_UPLOAD = "PUT";
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
* Constructs a new instance of the GitPermissionFilter.
*
* @param configuration
* @param repositoryProvider
* @param configuration scm main configuration
* @param repositoryProvider repository provider
*/
@Inject
public GitPermissionFilter(ScmConfiguration configuration,
RepositoryProvider repositoryProvider)
{
public GitPermissionFilter(ScmConfiguration configuration, RepositoryProvider repositoryProvider) {
super(configuration, repositoryProvider);
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param request
* @param response
*
* @throws IOException
*/
@Override
protected void sendNotEnoughPrivilegesError(HttpServletRequest request,
HttpServletResponse response)
throws IOException
{
if (GitUtil.isGitClient(request))
{
protected void sendNotEnoughPrivilegesError(HttpServletRequest request, HttpServletResponse response)
throws IOException {
if (GitUtil.isGitClient(request)) {
GitSmartHttpTools.sendError(request, response,
HttpServletResponse.SC_FORBIDDEN,
ClientMessages.get(request).notEnoughPrivileges());
}
else
{
} else {
super.sendNotEnoughPrivilegesError(request, response);
}
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param request
*
* @return
*/
@Override
protected boolean isWriteRequest(HttpServletRequest request)
{
String uri = request.getRequestURI();
return uri.endsWith(URI_RECEIVE_PACK)
|| (uri.endsWith(URI_REF_INFO)
&& PARAMETER_VALUE_RECEIVE.equals(
request.getParameter(PARAMETER_SERVICE)));
protected boolean isWriteRequest(HttpServletRequest request) {
return isReceivePackRequest(request) ||
isReceiveServiceRequest(request) ||
isLfsFileUpload(request);
}
private boolean isReceivePackRequest(HttpServletRequest request) {
return request.getRequestURI().endsWith(URI_RECEIVE_PACK);
}
private boolean isReceiveServiceRequest(HttpServletRequest request) {
return request.getRequestURI().endsWith(URI_REF_INFO)
&& PARAMETER_VALUE_RECEIVE.equals(request.getParameter(PARAMETER_SERVICE));
}
@VisibleForTesting
private static boolean isLfsFileUpload(HttpServletRequest request) {
return METHOD_LFS_UPLOAD.equalsIgnoreCase(request.getMethod());
}
}

View File

@@ -35,6 +35,7 @@ package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -63,13 +64,11 @@ import javax.servlet.http.HttpServletRequest;
*
* @author Sebastian Sdorra
*/
public class GitRepositoryResolver
implements RepositoryResolver<HttpServletRequest>
public class GitRepositoryResolver implements RepositoryResolver<HttpServletRequest>
{
/** the logger for GitRepositoryResolver */
private static final Logger logger =
LoggerFactory.getLogger(GitRepositoryResolver.class);
private static final Logger logger = LoggerFactory.getLogger(GitRepositoryResolver.class);
//~--- constructors ---------------------------------------------------------
@@ -114,20 +113,14 @@ public class GitRepositoryResolver
if (config.isValid())
{
File gitdir = new File(config.getRepositoryDirectory(), repositoryName);
if (logger.isDebugEnabled())
{
logger.debug("try to open git repository at {}", gitdir);
}
if (!gitdir.exists())
{
File gitdir = findRepository(config.getRepositoryDirectory(), repositoryName);
if (gitdir == null) {
throw new RepositoryNotFoundException(repositoryName);
}
logger.debug("try to open git repository at {}", gitdir);
repository = RepositoryCache.open(FileKey.lenient(gitdir, FS.DETECTED),
true);
repository = RepositoryCache.open(FileKey.lenient(gitdir, FS.DETECTED), true);
}
else
{
@@ -139,17 +132,39 @@ public class GitRepositoryResolver
throw new ServiceNotEnabledException();
}
}
catch (RuntimeException e)
{
throw new RepositoryNotFoundException(repositoryName, e);
}
catch (IOException e)
catch (RuntimeException | IOException e)
{
throw new RepositoryNotFoundException(repositoryName, e);
}
return repository;
}
@VisibleForTesting
File findRepository(File parentDirectory, String repositoryName) {
File repositoryDirectory = new File(parentDirectory, repositoryName);
if (repositoryDirectory.exists()) {
return repositoryDirectory;
}
if (endsWithDotGit(repositoryName)) {
String repositoryNameWithoutDotGit = repositoryNameWithoutDotGit(repositoryName);
repositoryDirectory = new File(parentDirectory, repositoryNameWithoutDotGit);
if (repositoryDirectory.exists()) {
return repositoryDirectory;
}
}
return null;
}
private boolean endsWithDotGit(String repositoryName) {
return repositoryName.endsWith(GitRepositoryHandler.DOT_GIT);
}
private String repositoryNameWithoutDotGit(String repositoryName) {
return repositoryName.substring(0, repositoryName.length() - GitRepositoryHandler.DOT_GIT.length());
}
//~--- fields ---------------------------------------------------------------

View File

@@ -41,6 +41,8 @@ import org.eclipse.jgit.transport.ScmTransportProtocol;
import sonia.scm.plugin.Extension;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
/**
*
* @author Sebastian Sdorra
@@ -49,8 +51,11 @@ import sonia.scm.plugin.Extension;
public class GitServletModule extends ServletModule
{
public static final String GIT_PATH = "/git";
/** Field description */
public static final String PATTERN_GIT = "/git/*";
public static final String PATTERN_GIT = GIT_PATH + "/*";
//~--- methods --------------------------------------------------------------
@@ -65,6 +70,8 @@ public class GitServletModule extends ServletModule
bind(GitRepositoryResolver.class);
bind(GitReceivePackFactory.class);
bind(ScmTransportProtocol.class);
bind(LfsBlobStoreFactory.class);
// serlvelts and filters
serve(PATTERN_GIT).with(ScmGitServlet.class);

View File

@@ -35,63 +35,89 @@ package sonia.scm.web;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import java.util.Locale;
import sonia.scm.plugin.Extension;
/**
*
* UserAgent provider for git related clients.
* @author Sebastian Sdorra <sebastian.sdorra@gmail.com>
* @since 1.45
*/
@Extension
public class GitUserAgentProvider implements UserAgentProvider
{
public class GitUserAgentProvider implements UserAgentProvider {
private static final String PREFIX_JGIT = "jgit/";
/** Field description */
@VisibleForTesting
static final UserAgent GIT = UserAgent.builder("Git").browser(
false).basicAuthenticationCharset(
Charsets.UTF_8).build();
/** Field description */
static final UserAgent JGIT = UserAgent.builder("JGit")
.browser(false)
.basicAuthenticationCharset(Charsets.UTF_8)
.build();
private static final String PREFIX_REGULAR = "git/";
@VisibleForTesting
static final UserAgent MSYSGIT = UserAgent.builder("msysGit").browser(
false).basicAuthenticationCharset(
Charsets.UTF_8).build();
static final UserAgent GIT = UserAgent.builder("Git")
.browser(false)
.basicAuthenticationCharset(Charsets.UTF_8)
.build();
private static final String PREFIX_LFS = "git-lfs/";
@VisibleForTesting
static final UserAgent GIT_LFS = UserAgent.builder("Git Lfs")
.browser(false)
.basicAuthenticationCharset(Charsets.UTF_8)
.build();
private static final String SUFFIX_MSYSGIT = "msysgit";
@VisibleForTesting
static final UserAgent MSYSGIT = UserAgent.builder("msysGit")
.browser(false)
.basicAuthenticationCharset(Charsets.UTF_8)
.build();
/** Field description */
private static final String PREFIX = "git/";
/** Field description */
private static final String SUFFIX = "msysgit";
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param userAgentString
*
* @return
*/
@Override
public UserAgent parseUserAgent(String userAgentString)
{
UserAgent ua = null;
if (userAgentString.startsWith(PREFIX))
{
if (userAgentString.contains(SUFFIX))
{
ua = MSYSGIT;
}
else
{
ua = GIT;
}
public UserAgent parseUserAgent(String userAgentString) {
String lowerUserAgent = toLower(userAgentString);
if (isJGit(lowerUserAgent)) {
return JGIT;
} else if (isMsysGit(lowerUserAgent)) {
return MSYSGIT;
} else if (isGitLFS(lowerUserAgent)) {
return GIT_LFS;
} else if (isGit(lowerUserAgent)) {
return GIT;
} else {
return null;
}
return ua;
}
private String toLower(String value) {
return Strings.nullToEmpty(value).toLowerCase(Locale.ENGLISH);
}
private boolean isJGit(String userAgent) {
return userAgent.startsWith(PREFIX_JGIT);
}
private boolean isMsysGit(String userAgent) {
return userAgent.startsWith(PREFIX_REGULAR) && userAgent.contains(SUFFIX_MSYSGIT);
}
private boolean isGitLFS(String userAgent) {
return userAgent.startsWith(PREFIX_LFS);
}
private boolean isGit(String userAgent) {
return userAgent.startsWith(PREFIX_REGULAR);
}
}

View File

@@ -35,23 +35,32 @@ package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.http.server.GitServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.slf4j.LoggerFactory.getLogger;
import org.eclipse.jgit.lfs.lib.Constants;
import static org.eclipse.jgit.lfs.lib.Constants.CONTENT_TYPE_GIT_LFS_JSON;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryProvider;
import sonia.scm.repository.RepositoryRequestListenerUtil;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.lfs.servlet.LfsServletFactory;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sonia.scm.repository.RepositoryException;
@@ -65,15 +74,15 @@ public class ScmGitServlet extends GitServlet
{
/** Field description */
public static final String REGEX_GITHTTPBACKEND =
"(?x)^/git/(.*/(HEAD|info/refs|objects/(info/[^/]+|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\\.(pack|idx))|git-(upload|receive)-pack))$";
public static final Pattern REGEX_GITHTTPBACKEND = Pattern.compile(
"(?x)^/git/(.*/(HEAD|info/refs|objects/(info/[^/]+|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\\.(pack|idx))|git-(upload|receive)-pack))$"
);
/** Field description */
private static final long serialVersionUID = -7712897339207470674L;
/** the logger for ScmGitServlet */
private static final Logger logger =
LoggerFactory.getLogger(ScmGitServlet.class);
private static final Logger logger = getLogger(ScmGitServlet.class);
//~--- constructors ---------------------------------------------------------
@@ -87,17 +96,21 @@ public class ScmGitServlet extends GitServlet
* @param repositoryViewer
* @param repositoryProvider
* @param repositoryRequestListenerUtil
* @param lfsServletFactory
*/
@Inject
public ScmGitServlet(GitRepositoryResolver repositoryResolver,
GitReceivePackFactory receivePackFactory,
GitRepositoryViewer repositoryViewer,
RepositoryProvider repositoryProvider,
RepositoryRequestListenerUtil repositoryRequestListenerUtil)
GitReceivePackFactory receivePackFactory,
GitRepositoryViewer repositoryViewer,
RepositoryProvider repositoryProvider,
RepositoryRequestListenerUtil repositoryRequestListenerUtil,
LfsServletFactory lfsServletFactory)
{
this.repositoryProvider = repositoryProvider;
this.repositoryViewer = repositoryViewer;
this.repositoryRequestListenerUtil = repositoryRequestListenerUtil;
this.lfsServletFactory = lfsServletFactory;
setRepositoryResolver(repositoryResolver);
setReceivePackFactory(receivePackFactory);
}
@@ -118,74 +131,165 @@ public class ScmGitServlet extends GitServlet
protected void service(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
String uri = HttpUtil.getStrippedURI(request);
if (uri.matches(REGEX_GITHTTPBACKEND))
{
sonia.scm.repository.Repository repository = repositoryProvider.get();
if (repository != null)
{
if (repositoryRequestListenerUtil.callListeners(request, response,
repository))
{
super.service(request, response);
}
else if (logger.isDebugEnabled())
{
logger.debug("request aborted by repository request listener");
}
}
else
{
super.service(request, response);
}
}
else
{
printGitInformation(request, response);
{
Repository repository = repositoryProvider.get();
if (repository != null) {
handleRequest(request, response, repository);
} else {
// logger
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
/**
* Decides the type request being currently made and delegates it accordingly.
* <ul>
* <li>Batch API:</li>
* <ul>
* <li>used to provide the client with information on how handle the large files of a repository.</li>
* <li>response contains the information where to perform the actual upload and download of the large objects.</li>
* </ul>
* <li>Transfer API:</li>
* <ul>
* <li>receives and provides the actual large objects (resolves the pointer placed in the file of the working copy).</li>
* <li>invoked only after the Batch API has been questioned about what to do with the large files</li>
* </ul>
* <li>Regular Git Http API:</li>
* <ul>
* <li>regular git http wire protocol, use by normal git clients.</li>
* </ul>
* <li>Browser Overview:<li>
* <ul>
* <li>short repository overview for browser clients.</li>
* </ul>
* </li>
* </ul>
*/
private void handleRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
logger.trace("handle git repository at {}", repository.getName());
if (isLfsBatchApiRequest(request, repository.getName())) {
HttpServlet servlet = lfsServletFactory.createProtocolServletFor(repository, request);
logger.trace("handle lfs batch api request");
handleGitLfsRequest(servlet, request, response, repository);
} else if (isLfsFileTransferRequest(request, repository.getName())) {
HttpServlet servlet = lfsServletFactory.createFileLfsServletFor(repository, request);
logger.trace("handle lfs file transfer request");
handleGitLfsRequest(servlet, request, response, repository);
} else if (isRegularGitAPIRequest(request)) {
logger.trace("handle regular git request");
// continue with the regular git Backend
handleRegularGitRequest(request, response, repository);
} else {
logger.trace("handle browser request");
handleBrowserRequest(request, response, repository);
}
}
private boolean isRegularGitAPIRequest(HttpServletRequest request) {
return REGEX_GITHTTPBACKEND.matcher(HttpUtil.getStrippedURI(request)).matches();
}
private void handleGitLfsRequest(HttpServlet servlet, HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
if (repositoryRequestListenerUtil.callListeners(request, response, repository)) {
servlet.service(request, response);
} else if (logger.isDebugEnabled()) {
logger.debug("request aborted by repository request listener");
}
}
private void handleRegularGitRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
if (repositoryRequestListenerUtil.callListeners(request, response, repository)) {
super.service(request, response);
} else if (logger.isDebugEnabled()) {
logger.debug("request aborted by repository request listener");
}
}
/**
* Method description
*
*
*
* This method renders basic information about the repository into the response. The result is meant to be viewed by
* browser.
* @param request
* @param response
*
* @throws IOException
* @throws ServletException
*/
private void printGitInformation(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
sonia.scm.repository.Repository scmRepository = repositoryProvider.get();
if (scmRepository != null)
{
try
{
repositoryViewer.handleRequest(request, response, scmRepository);
}
catch (RepositoryException ex)
{
throw new ServletException("could not create repository view", ex);
}
catch (IOException ex)
{
throw new ServletException("could not create repository view", ex);
}
}
else
{
response.sendError(HttpServletResponse.SC_NOT_FOUND);
private void handleBrowserRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
try {
repositoryViewer.handleRequest(request, response, repository);
} catch (RepositoryException | IOException ex) {
throw new ServletException("could not create repository view", ex);
}
}
/**
* Decides whether or not a request is for the LFS Batch API,
* <p>
* - PUT or GET
* - exactly for this repository
* - Content Type is {@link Constants#HDR_APPLICATION_OCTET_STREAM}.
*
* @return Returns {@code false} if either of the conditions does not match. Returns true if all match.
*/
private static boolean isLfsFileTransferRequest(HttpServletRequest request, String repository) {
String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/[a-z0-9]{64}$", request.getContextPath(), GitServletModule.GIT_PATH, repository);
boolean pathMatches = request.getRequestURI().matches(regex);
boolean methodMatches = request.getMethod().equals("PUT") || request.getMethod().equals("GET");
return pathMatches && methodMatches;
}
/**
* Decides whether or not a request is for the LFS Batch API,
* <p>
* - POST
* - exactly for this repository
* - Content Type is {@link Constants#CONTENT_TYPE_GIT_LFS_JSON}.
*
* @return Returns {@code false} if either of the conditions does not match. Returns true if all match.
*/
private static boolean isLfsBatchApiRequest(HttpServletRequest request, String repository) {
String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/batch$", request.getContextPath(), GitServletModule.GIT_PATH, repository);
boolean pathMatches = request.getRequestURI().matches(regex);
boolean methodMatches = "POST".equals(request.getMethod());
boolean headerContentTypeMatches = isLfsContentHeaderField(request.getContentType(), CONTENT_TYPE_GIT_LFS_JSON);
boolean headerAcceptMatches = isLfsContentHeaderField(request.getHeader("Accept"), CONTENT_TYPE_GIT_LFS_JSON);
return pathMatches && methodMatches && headerContentTypeMatches && headerAcceptMatches;
}
/**
* Checks whether request is of the specific content type.
*
* @param request The HTTP request header value to be examined.
* @param expectedContentType The expected content type.
* @return Returns {@code true} if the request has the expected content type. Return {@code false} otherwise.
*/
@VisibleForTesting
static boolean isLfsContentHeaderField(String request, String expectedContentType) {
if (request == null || request.isEmpty()) {
return false;
}
String[] parts = request.split(" ");
for (String part : parts) {
if (part.startsWith(expectedContentType)) {
return true;
}
}
return false;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@@ -194,6 +298,11 @@ public class ScmGitServlet extends GitServlet
/** Field description */
private final RepositoryRequestListenerUtil repositoryRequestListenerUtil;
/** Field description */
/**
* Field description
*/
private final GitRepositoryViewer repositoryViewer;
private final LfsServletFactory lfsServletFactory;
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web.lfs;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import sonia.scm.repository.Repository;
import sonia.scm.store.BlobStore;
import sonia.scm.store.BlobStoreFactory;
/**
* Creates {@link BlobStore} objects to store lfs objects.
*
* @author Sebastian Sdorra
* @since 1.54
*/
@Singleton
public class LfsBlobStoreFactory {
private static final String GIT_LFS_REPOSITORY_POSTFIX = "-git-lfs";
private final BlobStoreFactory blobStoreFactory;
/**
* Create a new instance.
*
* @param blobStoreFactory blob store factory
*/
@Inject
public LfsBlobStoreFactory(BlobStoreFactory blobStoreFactory) {
this.blobStoreFactory = blobStoreFactory;
}
/**
* Provides a {@link BlobStore} corresponding to the SCM Repository.
* <p>
* git-lfs repositories should generally carry the same name as their regular SCM repository counterparts. However,
* we have decided to store them under their IDs instead of their names, since the names might change and provide
* other drawbacks, as well.
* <p>
* These repositories will have {@linkplain #GIT_LFS_REPOSITORY_POSTFIX} appended to their IDs.
*
* @param repository The SCM Repository to provide a LFS {@link BlobStore} for.
*
* @return blob store for the corresponding scm repository
*/
public BlobStore getLfsBlobStore(Repository repository) {
return blobStoreFactory.getBlobStore(repository.getId() + GIT_LFS_REPOSITORY_POSTFIX);
}
}

View File

@@ -0,0 +1,97 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web.lfs;
import com.google.common.eventbus.Subscribe;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.EagerSingleton;
import sonia.scm.HandlerEventType;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryEvent;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
/**
* Listener which removes all lfs objects from a blob store, whenever its corresponding git repository gets deleted.
*
* @author Sebastian Sdorra
* @since 1.54
*/
@Extension
@EagerSingleton
public class LfsStoreRemoveListener {
private static final Logger LOG = LoggerFactory.getLogger(LfsBlobStoreFactory.class);
private final LfsBlobStoreFactory lfsBlobStoreFactory;
@Inject
public LfsStoreRemoveListener(LfsBlobStoreFactory lfsBlobStoreFactory) {
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
}
/**
* Remove all object from the blob store, if the event is an delete event and the repository is a git repository.
*
* @param event repository event
*/
@Subscribe
public void handleRepositoryEvent(RepositoryEvent event) {
if ( isDeleteEvent(event) && isGitRepositoryEvent(event) ) {
removeLfsStore(event.getItem());
}
}
private boolean isDeleteEvent(RepositoryEvent event) {
return HandlerEventType.DELETE == event.getEventType();
}
private boolean isGitRepositoryEvent(RepositoryEvent event) {
return event.getItem() != null
&& event.getItem().getType().equals(GitRepositoryHandler.TYPE_NAME);
}
private void removeLfsStore(Repository repository) {
LOG.debug("remove all blobs from store, because corresponding git repository {} was removed", repository.getName());
BlobStore blobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
for ( Blob blob : blobStore.getAll() ) {
LOG.trace("remove blob {}, because repository {} was removed", blob.getId(), repository.getName());
blobStore.remove(blob);
}
}
}

View File

@@ -0,0 +1,90 @@
package sonia.scm.web.lfs;
import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
import org.eclipse.jgit.lfs.server.LargeFileRepository;
import org.eclipse.jgit.lfs.server.Response;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
import java.io.IOException;
/**
* This LargeFileRepository is used for jGit-Servlet implementation. Under the jgit LFS Servlet hood, the
* SCM-Repository API is used to implement the Repository.
*
* @since 1.54
* Created by omilke on 03.05.2017.
*/
public class ScmBlobLfsRepository implements LargeFileRepository {
private final BlobStore blobStore;
/**
* This URI is used to determine the actual URI for Upload / Download. Must be full URI (or rewritable by reverse
* proxy).
*/
private final String baseUri;
/**
* Creates a {@link ScmBlobLfsRepository} for the provided repository.
*
* @param blobStore The SCM Blobstore used for this @{@link LargeFileRepository}.
* @param baseUri This URI is used to determine the actual URI for Upload / Download. Must be full URI (or
* rewritable by reverse proxy).
*/
public ScmBlobLfsRepository(BlobStore blobStore, String baseUri) {
this.blobStore = blobStore;
this.baseUri = baseUri;
}
@Override
public Response.Action getDownloadAction(AnyLongObjectId id) {
return getAction(id);
}
@Override
public Response.Action getUploadAction(AnyLongObjectId id, long size) {
return getAction(id);
}
@Override
public Response.Action getVerifyAction(AnyLongObjectId id) {
//validation is optional. We do not support it.
return null;
}
@Override
public long getSize(AnyLongObjectId id) throws IOException {
//this needs to be size of what is will be written into the response of the download. Clients are likely to
// verify it.
Blob blob = this.blobStore.get(id.getName());
if (blob == null) {
return -1;
} else {
return blob.getSize();
}
}
/**
* Constructs the Download / Upload actions to be supplied to the client.
*/
private Response.Action getAction(AnyLongObjectId id) {
//LFS protocol has to provide the information on where to put or get the actual content, i. e.
//the actual URI for up- and download.
Response.Action a = new Response.Action();
a.href = baseUri + id.getName();
return a;
}
}

View File

@@ -0,0 +1,76 @@
package sonia.scm.web.lfs.servlet;
import com.google.common.annotations.VisibleForTesting;
import org.eclipse.jgit.lfs.server.LargeFileRepository;
import org.eclipse.jgit.lfs.server.LfsProtocolServlet;
import org.eclipse.jgit.lfs.server.fs.FileLfsServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.Repository;
import sonia.scm.store.BlobStore;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.lfs.ScmBlobLfsRepository;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
/**
* This factory class is a helper class to provide the {@link LfsProtocolServlet} and the {@link FileLfsServlet}
* belonging to a SCM Repository.
*
* @since 1.54
* Created by omilke on 11.05.2017.
*/
@Singleton
public class LfsServletFactory {
private static final Logger logger = LoggerFactory.getLogger(LfsServletFactory.class);
private final LfsBlobStoreFactory lfsBlobStoreFactory;
@Inject
public LfsServletFactory(LfsBlobStoreFactory lfsBlobStoreFactory) {
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
}
/**
* Builds the {@link LfsProtocolServlet} (jgit API) for a SCM Repository.
*
* @param repository The SCM Repository to build the servlet for.
* @param request The {@link HttpServletRequest} the used to access the SCM Repository.
* @return The {@link LfsProtocolServlet} to provide the LFS Batch API for a SCM Repository.
*/
public LfsProtocolServlet createProtocolServletFor(Repository repository, HttpServletRequest request) {
BlobStore blobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
String baseUri = buildBaseUri(repository, request);
LargeFileRepository largeFileRepository = new ScmBlobLfsRepository(blobStore, baseUri);
return new ScmLfsProtocolServlet(largeFileRepository);
}
/**
* Builds the {@link FileLfsServlet} (jgit API) for a SCM Repository.
*
* @param repository The SCM Repository to build the servlet for.
* @param request The {@link HttpServletRequest} the used to access the SCM Repository.
* @return The {@link FileLfsServlet} to provide the LFS Upload / Download API for a SCM Repository.
*/
public HttpServlet createFileLfsServletFor(Repository repository, HttpServletRequest request) {
return new ScmFileTransferServlet(lfsBlobStoreFactory.getLfsBlobStore(repository));
}
/**
* Build the complete URI, under which the File Transfer API for this repository will be will be reachable.
*
* @param repository The repository to build the File Transfer URI for.
* @param request The request to construct the complete URI from.
*/
@VisibleForTesting
static String buildBaseUri(Repository repository, HttpServletRequest request) {
return String.format("%s/git/%s.git/info/lfs/objects/", HttpUtil.getCompleteUrl(request), repository.getName());
}
}

View File

@@ -0,0 +1,280 @@
package sonia.scm.web.lfs.servlet;
import com.google.common.annotations.VisibleForTesting;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.http.HttpStatus;
import org.eclipse.jgit.lfs.errors.CorruptLongObjectException;
import org.eclipse.jgit.lfs.errors.InvalidLongObjectIdException;
import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
import org.eclipse.jgit.lfs.lib.Constants;
import org.eclipse.jgit.lfs.lib.LongObjectId;
import org.eclipse.jgit.lfs.server.LfsProtocolServlet;
import org.eclipse.jgit.lfs.server.fs.FileLfsServlet;
import org.eclipse.jgit.lfs.server.internal.LfsServerText;
import org.eclipse.jgit.util.HttpSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
import sonia.scm.util.IOUtil;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.MessageFormat;
/**
* This Servlet provides the upload and download of files via git-lfs.
* <p>
* This implementation is based on {@link FileLfsServlet} but adjusted to work with
* servlet-2.5 instead of servlet-3.1.
* <p>
*
* @see FileLfsServlet
* @since 1.54
* Created by omilke on 15.05.2017.
*/
public class ScmFileTransferServlet extends HttpServlet {
private static final Logger logger = LoggerFactory.getLogger(ScmFileTransferServlet.class);
private static final long serialVersionUID = 1L;
/**
* Gson is used because the implementation was based on the jgit implementation. However the {@link LfsProtocolServlet} (which we do use in
* {@link ScmLfsProtocolServlet}) also uses Gson, which currently ties us to Gson anyway.
*/
private static Gson gson = createGson();
private final BlobStore blobStore;
public ScmFileTransferServlet(BlobStore store) {
this.blobStore = store;
}
/**
* Extracts the part after the last slash from path.
*
* @return Returns {@code null} if the part after the last slash is itself {@code null} or if its length is not 64.
*/
@VisibleForTesting
static String objectIdFromPath(String info) {
int lastSlash = info.lastIndexOf('/');
String potentialObjectId = info.substring(lastSlash + 1);
if (potentialObjectId.length() != 64) {
return null;
} else {
return potentialObjectId;
}
}
/**
* Logs the message and provides it to the client.
*
* @param response The response
* @param status The HTTP Status Code to be provided to the client.
* @param message the message to used for server-side logging. It is also provided to the client.
*/
private static void sendErrorAndLog(HttpServletResponse response, int status, String message) throws IOException {
logger.warn("Error occurred during git-lfs file transfer: {}", message);
sendError(response, status, message);
}
/**
* Logs the exception and provides only the message of the exception to the client.
*
* @param response The response
* @param status The HTTP Status Code to be provided to the client.
* @param exception An exception to used for server-side logging.
*/
private static void sendErrorAndLog(HttpServletResponse response, int status, Exception exception) throws IOException {
logger.warn("Error occurred during git-lfs file transfer.", exception);
String message = exception.getMessage();
sendError(response, status, message);
}
private static void sendError(HttpServletResponse response, int status, String message) throws IOException {
try (PrintWriter writer = response.getWriter()) {
gson.toJson(new Error(message), writer);
response.setStatus(status);
writer.flush();
}
response.flushBuffer();
}
private static Gson createGson() {
GsonBuilder gb = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setPrettyPrinting().disableHtmlEscaping();
return gb.create();
}
/**
* Provides a blob to download.
* <p>
* Actual implementation is based on <code>org.eclipse.jgit.lfs.server.fs.ObjectDownloadListener</code> and adjusted
* to non-async as we're currently on servlet-2.5.
*
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
AnyLongObjectId objectId = getObjectToTransfer(request, response);
if (objectId == null) {
logInvalidObjectId(request.getRequestURI());
} else {
final String objectIdName = objectId.getName();
logger.trace("---- providing download for LFS-Oid: {}", objectIdName);
Blob savedBlob = blobStore.get(objectIdName);
if (isBlobPresent(savedBlob)) {
logger.trace("----- Object {}: providing {} bytes", objectIdName, savedBlob.getSize());
writeBlobIntoResponse(savedBlob, response);
} else {
sendErrorAndLog(response, HttpStatus.SC_NOT_FOUND, MessageFormat.format(LfsServerText.get().objectNotFound, objectIdName));
}
}
}
/**
* Receives a blob from an upload.
* <p>
* Actual implementation is based on <code>org.eclipse.jgit.lfs.server.fs.ObjectUploadListener</code> and adjusted
* to non-async as we're currently on servlet-2.5.
*
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
@Override
protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
AnyLongObjectId objectId = getObjectToTransfer(request, response);
if (objectId == null) {
logInvalidObjectId(request.getRequestURI());
} else {
logger.trace("---- receiving upload for LFS-Oid: {}", objectId.getName());
readBlobFromResponse(request, response, objectId);
}
}
/**
* Extracts the {@link LongObjectId} from the request. Finishes the request, in case the {@link LongObjectId} cannot
* be extracted with an appropriate error.
*
* @throws IOException Thrown if the response could not be completed in an error case.
*/
private AnyLongObjectId getObjectToTransfer(HttpServletRequest request, HttpServletResponse response) throws IOException {
String path = request.getPathInfo();
String objectIdFromPath = objectIdFromPath(path);
if (objectIdFromPath == null) {
//ObjectId is not retrievable from URL
sendErrorAndLog(response, HttpStatus.SC_UNPROCESSABLE_ENTITY, MessageFormat.format(LfsServerText.get().invalidPathInfo, path));
return null;
} else {
try {
return LongObjectId.fromString(objectIdFromPath);
} catch (InvalidLongObjectIdException e) {
sendErrorAndLog(response, HttpStatus.SC_UNPROCESSABLE_ENTITY, e);
return null;
}
}
}
private void logInvalidObjectId(String requestURI) {
logger.warn("---- could not extract Oid from Request. Path seems to be invalid: {}", requestURI);
}
private boolean isBlobPresent(Blob savedBlob) {
return savedBlob != null && savedBlob.getSize() >= 0;
}
private void writeBlobIntoResponse(Blob savedBlob, HttpServletResponse response) throws IOException {
try (ServletOutputStream responseOutputStream = response.getOutputStream();
InputStream savedBlobInputStream = savedBlob.getInputStream()) {
response.addHeader(HttpSupport.HDR_CONTENT_LENGTH, String.valueOf(savedBlob.getSize()));
response.setContentType(Constants.HDR_APPLICATION_OCTET_STREAM);
IOUtil.copy(savedBlobInputStream, responseOutputStream);
} catch (IOException ex) {
sendErrorAndLog(response, HttpStatus.SC_INTERNAL_SERVER_ERROR, ex);
}
}
private void readBlobFromResponse(HttpServletRequest request, HttpServletResponse response, AnyLongObjectId objectId) throws IOException {
Blob blob = blobStore.create(objectId.getName());
try (OutputStream blobOutputStream = blob.getOutputStream();
ServletInputStream requestInputStream = request.getInputStream()) {
IOUtil.copy(requestInputStream, blobOutputStream);
blob.commit();
response.setContentType(Constants.CONTENT_TYPE_GIT_LFS_JSON);
response.setStatus(HttpServletResponse.SC_OK);
} catch (CorruptLongObjectException ex) {
sendErrorAndLog(response, HttpStatus.SC_BAD_REQUEST, ex);
}
}
/**
* Used for providing an error message.
*/
private static class Error {
String message;
Error(String m) {
this.message = m;
}
}
}

View File

@@ -0,0 +1,26 @@
package sonia.scm.web.lfs.servlet;
import org.eclipse.jgit.lfs.errors.LfsException;
import org.eclipse.jgit.lfs.server.LargeFileRepository;
import org.eclipse.jgit.lfs.server.LfsProtocolServlet;
/**
* Provides an implementation for the git-lfs Batch API.
*
* @since 1.54
* Created by omilke on 11.05.2017.
*/
public class ScmLfsProtocolServlet extends LfsProtocolServlet {
private final LargeFileRepository repository;
public ScmLfsProtocolServlet(LargeFileRepository largeFileRepository) {
this.repository = largeFileRepository;
}
@Override
protected LargeFileRepository getLargeFileRepository(LfsRequest request, String path) throws LfsException {
return repository;
}
}

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.repository;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Unit tests for {@link GitRepositoryPathMatcher}.
*
* @author Sebastian Sdorra
* @since 1.54
*/
public class GitRepositoryPathMatcherTest {
private final GitRepositoryPathMatcher pathMatcher = new GitRepositoryPathMatcher();
@Test
public void testIsPathMatching() {
assertFalse(pathMatcher.isPathMatching(repository("my-repo"), "my-repoo"));
assertFalse(pathMatcher.isPathMatching(repository("my"), "my-repo"));
assertFalse(pathMatcher.isPathMatching(repository("my"), "my-repo/with/path"));
assertTrue(pathMatcher.isPathMatching(repository("my-repo"), "my-repo"));
assertTrue(pathMatcher.isPathMatching(repository("my-repo"), "my-repo.git"));
assertTrue(pathMatcher.isPathMatching(repository("my-repo"), "my-repo/with/path"));
assertTrue(pathMatcher.isPathMatching(repository("my-repo"), "my-repo.git/with/path"));
}
private Repository repository(String name) {
return new Repository(name, GitRepositoryHandler.TYPE_NAME, name);
}
}

View File

@@ -35,15 +35,10 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@@ -52,8 +47,9 @@ import static org.mockito.Mockito.*;
import java.io.File;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import static org.junit.Assert.*;
import sonia.scm.util.HttpUtil;
/**
* Unit tests for {@link GitUtil}.
@@ -125,9 +121,25 @@ public class GitUtilTest
return repo;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@Rule
public TemporaryFolder temp = new TemporaryFolder();
@Test
public void testIsGitClient() {
HttpServletRequest request = mockRequestWithUserAgent("Git/2.9.3");
assertTrue(GitUtil.isGitClient(request));
request = mockRequestWithUserAgent("JGit/2.9.3");
assertTrue(GitUtil.isGitClient(request));
request = mockRequestWithUserAgent("Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) ...");
assertFalse(GitUtil.isGitClient(request));
}
private HttpServletRequest mockRequestWithUserAgent(String userAgent) {
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getHeader(HttpUtil.HEADER_USERAGENT)).thenReturn(userAgent);
return request;
}
}

View File

@@ -191,7 +191,7 @@ public class GitRepositoryClientProvider extends RepositoryClientProvider
@Override
public File getWorkingCopy() {
return git.getRepository().getDirectory();
return git.getRepository().getWorkTree();
}
//~--- fields ---------------------------------------------------------------

View File

@@ -0,0 +1,135 @@
package sonia.scm.web;
import com.google.common.base.Charsets;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import javax.servlet.ServletOutputStream;
import org.junit.Test;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import static org.mockito.Mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.RepositoryProvider;
import sonia.scm.util.HttpUtil;
/**
* Unit tests for {@link GitPermissionFilter}.
*
* Created by omilke on 19.05.2017.
*/
@RunWith(MockitoJUnitRunner.class)
public class GitPermissionFilterTest {
@Mock
private RepositoryProvider repositoryProvider;
private final GitPermissionFilter permissionFilter = new GitPermissionFilter(
new ScmConfiguration(), repositoryProvider
);
@Mock
private HttpServletResponse response;
@Test
public void testIsWriteRequest() {
HttpServletRequest request = mockRequestWithMethodAndRequestURI("POST", "/scm/git/fanzy-project/git-receive-pack");
assertThat(permissionFilter.isWriteRequest(request), is(true));
request = mockRequestWithMethodAndRequestURI("GET", "/scm/git/fanzy-project/info/refs?service=git-receive-pack");
assertThat(permissionFilter.isWriteRequest(request), is(true));
request = mockRequestWithMethodAndRequestURI("GET", "/scm/git/fanzy-project/info/refs?service=some-other-service");
assertThat(permissionFilter.isWriteRequest(request), is(false));
request = mockRequestWithMethodAndRequestURI(
"PUT",
"/scm/git/git-lfs-demo.git/info/lfs/objects/8fcebeb5698230685f92028e560f8f1683ebc15ec82a620ffad5c12a3c19bdec"
);
assertThat(permissionFilter.isWriteRequest(request), is(true));
request = mockRequestWithMethodAndRequestURI(
"GET",
"/scm/git/git-lfs-demo.git/info/lfs/objects/8fcebeb5698230685f92028e560f8f1683ebc15ec82a620ffad5c12a3c19bdec"
);
assertThat(permissionFilter.isWriteRequest(request), is(false));
request = mockRequestWithMethodAndRequestURI("POST", "/scm/git/git-lfs-demo.git/info/lfs/objects/batch");
assertThat(permissionFilter.isWriteRequest(request), is(false));
}
private HttpServletRequest mockRequestWithMethodAndRequestURI(String method, String requestURI) {
HttpServletRequest mock = mock(HttpServletRequest.class);
when(mock.getMethod()).thenReturn(method);
when(mock.getRequestURI()).thenReturn(requestURI);
when(mock.getContextPath()).thenReturn("/scm");
return mock;
}
@Test
public void testSendNotEnoughPrivilegesErrorAsBrowser() throws IOException {
HttpServletRequest request = mockGitReceivePackServiceRequest();
permissionFilter.sendNotEnoughPrivilegesError(request, response);
verify(response).sendError(HttpServletResponse.SC_FORBIDDEN);
}
@Test
public void testSendNotEnoughPrivilegesErrorAsGitClient() throws IOException {
verifySendNotEnoughPrivilegesErrorAsGitClient("git/2.9.3");
}
@Test
public void testSendNotEnoughPrivilegesErrorAsJGitClient() throws IOException {
verifySendNotEnoughPrivilegesErrorAsGitClient("JGit/4.2");
}
private void verifySendNotEnoughPrivilegesErrorAsGitClient(String userAgent) throws IOException {
HttpServletRequest request = mockGitReceivePackServiceRequest();
when(request.getHeader(HttpUtil.HEADER_USERAGENT)).thenReturn(userAgent);
CapturingServletOutputStream stream = new CapturingServletOutputStream();
when(response.getOutputStream()).thenReturn(stream);
permissionFilter.sendNotEnoughPrivilegesError(request, response);
verify(response).setStatus(HttpServletResponse.SC_OK);
assertThat(stream.toString(), containsString("privileges"));
}
private HttpServletRequest mockGitReceivePackServiceRequest() {
HttpServletRequest request = mockRequestWithMethodAndRequestURI("GET", "/git/info/refs");
when(request.getParameter("service")).thenReturn("git-receive-pack");
return request;
}
private static class CapturingServletOutputStream extends ServletOutputStream {
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@Override
public void write(int b) throws IOException {
baos.write(b);
}
@Override
public void close() throws IOException {
baos.close();
}
@Override
public String toString() {
return baos.toString();
}
}
}

View File

@@ -0,0 +1,109 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web;
import java.io.File;
import java.io.IOException;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.mockito.Mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryHandler;
/**
* Unit tests for {@link GitRepositoryResolver}.
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
public class GitRepositoryResolverTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private File parentDirectory;
@Mock
private GitRepositoryHandler handler;
@InjectMocks
private GitRepositoryResolver resolver;
@Before
public void setUp() throws IOException {
parentDirectory = temporaryFolder.newFolder();
GitConfig config = new GitConfig();
config.setRepositoryDirectory(parentDirectory);
when(handler.getConfig()).thenReturn(config);
}
@Test
public void testFindRepositoryWithoutDotGit() {
createRepositories("a", "ab");
File directory = resolver.findRepository(parentDirectory, "a");
assertNotNull(directory);
assertEquals("a", directory.getName());
directory = resolver.findRepository(parentDirectory, "ab");
assertNotNull(directory);
assertEquals("ab", directory.getName());
}
@Test
public void testFindRepositoryWithDotGit() {
createRepositories("a", "ab");
File directory = resolver.findRepository(parentDirectory, "a.git");
assertNotNull(directory);
assertEquals("a", directory.getName());
directory = resolver.findRepository(parentDirectory, "ab.git");
assertNotNull(directory);
assertEquals("ab", directory.getName());
}
private void createRepositories(String... names) {
for (String name : names) {
assertTrue(new File(parentDirectory, name).mkdirs());
}
}
}

View File

@@ -33,51 +33,46 @@ package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Strings;
import org.junit.Test;
import static org.junit.Assert.*;
//~--- JDK imports ------------------------------------------------------------
import java.util.Locale;
/**
*
* Unit tests for {@link GitUserAgentProvider}.
*
* @author Sebastian Sdorra <sebastian.sdorra@triology.de>
*/
public class GitUserAgentProviderTest
{
public class GitUserAgentProviderTest {
/**
* Method description
*
*/
private final GitUserAgentProvider provider = new GitUserAgentProvider();
@Test
public void testParseUserAgent()
{
public void testParseUserAgent() {
assertEquals(GitUserAgentProvider.GIT, parse("git/1.7.9.5"));
assertEquals(GitUserAgentProvider.JGIT, parse("jgit/4.5.2"));
assertEquals(GitUserAgentProvider.GIT_LFS, parse("git-lfs/2.0.1 (GitHub; windows amd64; go 1.8; git 678cdbd4)"));
assertEquals(GitUserAgentProvider.MSYSGIT, parse("git/1.8.3.msysgit.0"));
assertNull(parse("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"));
}
/**
* Method description
*
*
* @param v
*
* @return
*/
private UserAgent parse(String v)
{
return provider.parseUserAgent(
Strings.nullToEmpty(v).toLowerCase(Locale.ENGLISH));
@Test
public void testParseUserAgentCaseSensitive() {
assertEquals(GitUserAgentProvider.GIT, parse("Git/1.7.9.5"));
}
@Test
public void testParseUserAgentWithEmptyValue() {
assertNull(parse(null));
}
@Test
public void testParseUserAgentWithNullValue() {
assertNull(parse(null));
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final GitUserAgentProvider provider = new GitUserAgentProvider();
private UserAgent parse(String v) {
return provider.parseUserAgent(v);
}
}

View File

@@ -0,0 +1,26 @@
package sonia.scm.web;
import org.junit.Test;
import static org.eclipse.jgit.lfs.lib.Constants.CONTENT_TYPE_GIT_LFS_JSON;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
/**
* Created by omilke on 11.05.2017.
*/
public class ScmGitServletTest {
@Test
public void isContentTypeMatches() throws Exception {
assertThat(ScmGitServlet.isLfsContentHeaderField("application/vnd.git-lfs+json", CONTENT_TYPE_GIT_LFS_JSON), is(true));
assertThat(ScmGitServlet.isLfsContentHeaderField("application/vnd.git-lfs+json;", CONTENT_TYPE_GIT_LFS_JSON), is(true));
assertThat(ScmGitServlet.isLfsContentHeaderField("application/vnd.git-lfs+json; charset=utf-8", CONTENT_TYPE_GIT_LFS_JSON), is(true));
assertThat(ScmGitServlet.isLfsContentHeaderField("application/vnd.git-lfs-json;", CONTENT_TYPE_GIT_LFS_JSON), is(false));
assertThat(ScmGitServlet.isLfsContentHeaderField("", CONTENT_TYPE_GIT_LFS_JSON), is(false));
assertThat(ScmGitServlet.isLfsContentHeaderField(null, CONTENT_TYPE_GIT_LFS_JSON), is(false));
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web.lfs;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import static org.mockito.Matchers.matches;
import org.mockito.Mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import org.mockito.runners.MockitoJUnitRunner;
import sonia.scm.repository.Repository;
import sonia.scm.store.BlobStoreFactory;
/**
* Unit tests for {@link LfsBlobStoreFactory}.
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
public class LfsBlobStoreFactoryTest {
@Mock
private BlobStoreFactory blobStoreFactory;
@InjectMocks
private LfsBlobStoreFactory lfsBlobStoreFactory;
@Test
public void getBlobStore() throws Exception {
lfsBlobStoreFactory.getLfsBlobStore(new Repository("the-id", "GIT", "the-name"));
// just make sure the right parameter is passed, as properly validating the return value is nearly impossible with
// the return value (and should not be part of this test)
verify(blobStoreFactory).getBlobStore(matches("the-id-git-lfs"));
// make sure there have been no further usages of the factory
verifyNoMoreInteractions(blobStoreFactory);
}
}

View File

@@ -0,0 +1,122 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web.lfs;
import com.google.common.collect.Lists;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.mockito.Mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import sonia.scm.HandlerEventType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryEvent;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
/**
* Unit tests for {@link LfsStoreRemoveListener}.
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
public class LfsStoreRemoveListenerTest {
@Mock
private LfsBlobStoreFactory lfsBlobStoreFactory;
@Mock
private BlobStore blobStore;
@InjectMocks
private LfsStoreRemoveListener lfsStoreRemoveListener;
@Test
public void testHandleRepositoryEventWithNonDeleteEvents() {
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_CREATE));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.CREATE));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_MODIFY));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.MODIFY));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_DELETE));
verifyZeroInteractions(lfsBlobStoreFactory);
}
@Test
public void testHandleRepositoryEventWithNonGitRepositories() {
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "svn"));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "hg"));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "dummy"));
verifyZeroInteractions(lfsBlobStoreFactory);
}
@Test
public void testHandleRepositoryEvent() {
Repository heartOfGold = RepositoryTestData.createHeartOfGold("git");
when(lfsBlobStoreFactory.getLfsBlobStore(heartOfGold)).thenReturn(blobStore);
Blob blobA = mockBlob("a");
Blob blobB = mockBlob("b");
List<Blob> blobs = Lists.newArrayList(blobA, blobB);
when(blobStore.getAll()).thenReturn(blobs);
lfsStoreRemoveListener.handleRepositoryEvent(new RepositoryEvent(HandlerEventType.DELETE, heartOfGold));
verify(blobStore).getAll();
verify(blobStore).remove(blobA);
verify(blobStore).remove(blobB);
verifyNoMoreInteractions(blobStore);
}
private Blob mockBlob(String id) {
Blob blob = mock(Blob.class);
when(blob.getId()).thenReturn(id);
return blob;
}
private RepositoryEvent event(HandlerEventType eventType) {
return event(eventType, "git");
}
private RepositoryEvent event(HandlerEventType eventType, String repositoryType) {
return new RepositoryEvent(eventType, RepositoryTestData.create42Puzzle(repositoryType));
}
}

View File

@@ -0,0 +1,53 @@
package sonia.scm.web.lfs.servlet;
import org.junit.Test;
import sonia.scm.repository.Repository;
import javax.servlet.http.HttpServletRequest;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* Created by omilke on 18.05.2017.
*/
public class LfsServletFactoryTest {
@Test
public void buildBaseUri() throws Exception {
String repositoryName = "git-lfs-demo";
String result = LfsServletFactory.buildBaseUri(new Repository("", "GIT", repositoryName), RequestWithUri(repositoryName, true));
assertThat(result, is(equalTo("http://localhost:8081/scm/git/git-lfs-demo.git/info/lfs/objects/")));
//result will be with dot-gix suffix, ide
result = LfsServletFactory.buildBaseUri(new Repository("", "GIT", repositoryName), RequestWithUri(repositoryName, false));
assertThat(result, is(equalTo("http://localhost:8081/scm/git/git-lfs-demo.git/info/lfs/objects/")));
}
private HttpServletRequest RequestWithUri(String repositoryName, boolean withDotGitSuffix) {
HttpServletRequest mockedRequest = mock(HttpServletRequest.class);
final String suffix;
if (withDotGitSuffix) {
suffix = ".git";
} else {
suffix = "";
}
//build from valid live request data
when(mockedRequest.getRequestURL()).thenReturn(
new StringBuffer(String.format("http://localhost:8081/scm/git/%s%s/info/lfs/objects/batch", repositoryName, suffix)));
when(mockedRequest.getRequestURI()).thenReturn(String.format("/scm/git/%s%s/info/lfs/objects/batch", repositoryName, suffix));
when(mockedRequest.getContextPath()).thenReturn("/scm");
return mockedRequest;
}
}

View File

@@ -0,0 +1,42 @@
package sonia.scm.web.lfs.servlet;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.*;
/**
* Created by omilke on 16.05.2017.
*/
public class ScmFileTransferServletTest {
@Test
public void hasObjectId() throws Exception {
String SAMPLE_OBJECT_ID = "8fcebeb5698230685f92028e560f8f1683ebc15ec82a620ffad5c12a3c19bdec";
String path = "/git-lfs-demo.git/info/lfs/objects/" + SAMPLE_OBJECT_ID;
assertThat(ScmFileTransferServlet.objectIdFromPath(path), is(equalTo(SAMPLE_OBJECT_ID)));
path = "/" + SAMPLE_OBJECT_ID;
assertThat(ScmFileTransferServlet.objectIdFromPath(path), is(equalTo(SAMPLE_OBJECT_ID)));
path = SAMPLE_OBJECT_ID;
assertThat(ScmFileTransferServlet.objectIdFromPath(path), is(equalTo(SAMPLE_OBJECT_ID)));
String nonObjectId = "this-ist-last-to-found";
path = "/git-lfs-demo.git/info/lfs/objects/" + nonObjectId;
assertThat(ScmFileTransferServlet.objectIdFromPath(path), is(nullValue()));
nonObjectId = SAMPLE_OBJECT_ID.substring(1);
path = "/git-lfs-demo.git/info/lfs/objects/" + nonObjectId;
assertThat(ScmFileTransferServlet.objectIdFromPath(path), is(nullValue()));
nonObjectId = SAMPLE_OBJECT_ID + "X";
path = "/git-lfs-demo.git/info/lfs/objects/" + nonObjectId;
assertThat(ScmFileTransferServlet.objectIdFromPath(path), is(nullValue()));
}
}

View File

@@ -22,7 +22,7 @@
<dependency>
<groupId>com.aragost.javahg</groupId>
<artifactId>javahg</artifactId>
<version>0.7-scm1</version>
<version>0.8-scm1</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>

View File

@@ -32,7 +32,10 @@
Prints date, size and last message of files.
"""
from mercurial import util
from mercurial import cmdutil,util
cmdtable = {}
command = cmdutil.command(cmdtable)
class SubRepository:
url = None
@@ -133,6 +136,14 @@ def printFile(ui, repo, file, disableLastCommit, transport):
format = 'f%s\n%i %s %s\0'
ui.write( format % (file.path(), file.size(), date, description) )
@command('fileview', [
('r', 'revision', 'tip', 'revision to print'),
('p', 'path', '', 'path to print'),
('c', 'recursive', False, 'browse repository recursive'),
('d', 'disableLastCommit', False, 'disables last commit description and date'),
('s', 'disableSubRepositoryDetection', False, 'disables detection of sub repositories'),
('t', 'transport', False, 'format the output for command server'),
])
def fileview(ui, repo, **opts):
files = []
directories = []
@@ -154,15 +165,3 @@ def fileview(ui, repo, **opts):
printDirectory(ui, d, transport)
for f in files:
printFile(ui, repo, f, opts['disableLastCommit'], transport)
cmdtable = {
# cmd name function call
'fileview': (fileview,[
('r', 'revision', 'tip', 'revision to print'),
('p', 'path', '', 'path to print'),
('c', 'recursive', False, 'browse repository recursive'),
('d', 'disableLastCommit', False, 'disables last commit description and date'),
('s', 'disableSubRepositoryDetection', False, 'disables detection of sub repositories'),
('t', 'transport', False, 'format the output for command server'),
])
}

View File

@@ -172,7 +172,7 @@
<plugin>
<groupId>com.github.sdorra</groupId>
<artifactId>nativepkg-maven-plugin</artifactId>
<version>1.1.3</version>
<version>1.1.4</version>
<executions>
<execution>
<goals>
@@ -204,6 +204,7 @@
</platform>
<scripts>
<preInstall>${project.basedir}/src/main/nativepkg/create-user</preInstall>
<postInstall>${project.basedir}/src/main/nativepkg/clear-cache</postInstall>
</scripts>
<mappings>
<files>

View File

@@ -0,0 +1,9 @@
#!/bin/sh
# clear workdir after upgrade
# https://bitbucket.org/sdorra/scm-manager/issues/923/scmmanager-installed-from-debian-package
WORKDIR="/var/cache/scm/work/webapp"
if [ -d "${WORKDIR}" ]; then
rm -rf "${WORKDIR}"
fi

View File

@@ -63,12 +63,6 @@
<name>tmatesoft release repository</name>
<url>https://maven.tmatesoft.com/content/repositories/releases</url>
</repository>
<repository>
<id>jgit-repository</id>
<name>jgit release repository</name>
<url>http://download.eclipse.org/jgit/maven</url>
</repository>
</repositories>

View File

@@ -55,7 +55,9 @@ import static org.junit.Assert.*;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import java.util.logging.Logger;
/**
*
@@ -155,7 +157,11 @@ public class AbstractTestBase
}
finally
{
IOUtil.delete(tempDirectory);
try {
IOUtil.delete(tempDirectory);
} catch (IOException e) {
Logger.getGlobal().warning(String.format("deleting temp <%s> failed: %s", tempDirectory.getAbsolutePath(), e.getMessage()));
}
}
}

View File

@@ -218,20 +218,9 @@
<!-- rest documentation -->
<dependency>
<groupId>org.codehaus.enunciate</groupId>
<artifactId>enunciate-jersey-rt</artifactId>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-core-annotations</artifactId>
<version>${enunciate.version}</version>
<!-- fix conflict with jersey-json -->
<exclusions>
<exclusion>
<artifactId>jackson-jaxrs</artifactId>
<groupId>org.codehaus.jackson</groupId>
</exclusion>
<exclusion>
<artifactId>jackson-xc</artifactId>
<groupId>org.codehaus.jackson</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- test scope -->
@@ -545,10 +534,14 @@
<scm.home>target/scm-it</scm.home>
<environment.profile>default</environment.profile>
<selenium.version>2.53.1</selenium.version>
<enunciate.version>1.31</enunciate.version>
<enunciate.version>2.9.1</enunciate.version>
<wagon.version>1.0</wagon.version>
<mustache.version>0.8.17</mustache.version>
<netbeans.hint.deploy.server>Tomcat</netbeans.hint.deploy.server>
<sonar.issue.ignore.multicriteria>e1</sonar.issue.ignore.multicriteria>
<sonar.issue.ignore.multicriteria.e1.ruleKey>javascript:S3827</sonar.issue.ignore.multicriteria.e1.ruleKey>
<sonar.issue.ignore.multicriteria.e1.resourceKey>**.js</sonar.issue.ignore.multicriteria.e1.resourceKey>
<sonar.exclusions>src/main/webapp/resources/extjs/**,src/main/webapp/resources/moment/**,src/main/webapp/resources/syntaxhighlighter/**</sonar.exclusions>
</properties>
<profiles>
@@ -793,8 +786,35 @@
<plugins>
<plugin>
<groupId>org.codehaus.enunciate</groupId>
<artifactId>maven-enunciate-plugin</artifactId>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>copy-enunciate-configuration</id>
<phase>compile</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
<resources>
<resource>
<directory>src/main/doc</directory>
<filtering>true</filtering>
<includes>
<include>**/enunciate.xml</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-maven-plugin</artifactId>
<version>${enunciate.version}</version>
<executions>
<execution>
@@ -805,14 +825,21 @@
</execution>
</executions>
<configuration>
<configFile>src/main/doc/enunciate.xml</configFile>
<docsDir>${project.build.directory}/restdocs</docsDir>
<configFile>${project.build.directory}/enunciate.xml</configFile>
<docsDir>${project.build.directory}</docsDir>
<docsSubdir>restdocs</docsSubdir>
</configuration>
<dependencies>
<dependency>
<groupId>org.codehaus.enunciate</groupId>
<artifactId>enunciate-jersey</artifactId>
<version>${enunciate.version}</version>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-top</artifactId>
<version>2.9.1</version>
<exclusions>
<exclusion>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-swagger</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</plugin>

View File

@@ -39,30 +39,33 @@
Description: Enunciate configuration
-->
<enunciate label="full" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://enunciate.codehaus.org/schemas/enunciate-1.27.xsd">
<enunciate xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://enunciate.webcohesion.com/schemas/enunciate-2.9.0.xsd"
slug="scm-manager" version="${project.version}">
<title>SCM-Manager API</title>
<description>
<![CDATA[
<h1>SCM-Manager API</h1>
<p>This page describes the RESTful Web Service API of <a href="https://www.scm-manager.org">SCM-Manager</a> ${project.version}.</p>
]]>
</description>
<api-classes>
<include pattern="sonia.scm.api.rest.resources.*" />
<exclude pattern="sonia.scm.debug.DebugResource" />
<exclude pattern="sonia.scm.api.rest.resources.ConfigurationResource" />
<exclude pattern="sonia.scm.api.rest.resources.SupportResource" />
<exclude pattern="sonia.scm.api.rest.resources.RepositoryRootResource" />
</api-classes>
<services>
<rest defaultRestSubcontext="/api/rest" />
</services>
<modules>
<docs title="SCM-Manager API" />
<jaxrs datatype-detection="local">
<application path="/api/rest" />
</jaxrs>
<jersey resourceProviderFactory="com.sun.jersey.guice.spi.container.GuiceComponentProviderFactory">
<init-param name="com.sun.jersey.api.json.POJOMappingFeature" value="true" />
<init-param name="com.sun.jersey.config.feature.Redirect" value="true" />
<init-param name="com.sun.jersey.config.property.resourceConfigClass" value="sonia.scm.api.rest.UriExtensionsConfig" />
<init-param name="com.sun.jersey.config.property.packages" value="sonia.scm.api.rest" />
</jersey>
<docs disableResourceLinks="true" includeApplicationPath="true" />
</modules>

View File

@@ -137,6 +137,7 @@ import sonia.scm.schedule.QuartzScheduler;
import sonia.scm.schedule.Scheduler;
import sonia.scm.security.ConfigurableLoginAttemptHandler;
import sonia.scm.security.LoginAttemptHandler;
import sonia.scm.security.AuthorizationChangedEventProducer;
import sonia.scm.web.UserAgentParser;
/**
@@ -276,11 +277,14 @@ public class ScmServletModule extends JerseyServletModule
// bind security stuff
bind(LoginAttemptHandler.class).to(ConfigurableLoginAttemptHandler.class);
bind(AuthorizationChangedEventProducer.class);
bind(SecuritySystem.class).to(DefaultSecuritySystem.class);
bind(AdministrationContext.class, DefaultAdministrationContext.class);
// bind cache
bind(CacheManager.class, GuavaCacheManager.class);
bind(org.apache.shiro.cache.CacheManager.class, GuavaCacheManager.class);
// bind dao
bind(GroupDAO.class, XmlGroupDAO.class);

View File

@@ -38,6 +38,10 @@ package sonia.scm.api.rest.resources;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.api.rest.Permission;
import sonia.scm.security.AssignedPermission;
@@ -114,13 +118,7 @@ public abstract class AbstractPermissionResource
//~--- methods --------------------------------------------------------------
/**
* Adds a new permission to the user or group managed by the resource.<br />
* <br />
* Status codes:
* <ul>
* <li>201 add successful</li>
* <li>500 internal server error</li>
* </ul>
* Adds a new permission to the user or group managed by the resource.
*
* @param uriInfo uri informations
* @param permission permission to add
@@ -128,6 +126,13 @@ public abstract class AbstractPermissionResource
* @return web response
*/
@POST
@StatusCodes({
@ResponseCode(code = 201, condition = "creates", additionalHeaders = {
@ResponseHeader(name = "Location", description = "uri to new create permission")
}),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response add(@Context UriInfo uriInfo, Permission permission)
{
@@ -139,15 +144,7 @@ public abstract class AbstractPermissionResource
}
/**
* Deletes a permission from the user or group managed by the resource.<br />
* <br />
* Status codes:
* <ul>
* <li>200 delete successful</li>
* <li>400 bad request, permission id does not belong to the user or group</li>
* <li>404 not found, no permission with the specified id available</li>
* <li>500 internal server error</li>
* </ul>
* Deletes a permission from the user or group managed by the resource.
*
* @param id id of the permission
*
@@ -155,6 +152,13 @@ public abstract class AbstractPermissionResource
*/
@DELETE
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, permission id does not belong to the user or group"),
@ResponseCode(code = 404, condition = "not found, no permission with the specified id available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response delete(@PathParam("id") String id)
{
StoredAssignedPermission sap = getPermission(id);
@@ -165,16 +169,7 @@ public abstract class AbstractPermissionResource
}
/**
* Updates the specified permission on the user or group managed by the
* resource.<br />
* <br />
* Status codes:
* <ul>
* <li>204 update successful</li>
* <li>400 bad request, permission id does not belong to the user or group</li>
* <li>404 not found, no permission with the specified id available</li>
* <li>500 internal server error</li>
* </ul>
* Updates the specified permission on the user or group managed by the resource.
*
* @param id id of the permission
* @param permission updated permission
@@ -183,6 +178,13 @@ public abstract class AbstractPermissionResource
*/
@PUT
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, permission id does not belong to the user or group"),
@ResponseCode(code = 404, condition = "not found, no permission with the specified id available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response update(@PathParam("id") String id, Permission permission)
{
@@ -197,16 +199,7 @@ public abstract class AbstractPermissionResource
//~--- get methods ----------------------------------------------------------
/**
* Returns the {@link Permission} with the specified id.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>400 bad request, permission id does not belong to the user or group</li>
* <li>404 not found, no permission with the specified id available</li>
* <li>500 internal server error</li>
* </ul>
*
* Returns the {@link Permission} with the specified id.
*
* @param id id of the {@link Permission}
*
@@ -214,6 +207,12 @@ public abstract class AbstractPermissionResource
*/
@GET
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, permission id does not belong to the user or group"),
@ResponseCode(code = 404, condition = "not found, no permission with the specified id available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Permission get(@PathParam("id") String id)
{
@@ -223,17 +222,15 @@ public abstract class AbstractPermissionResource
}
/**
* Returns all permissions of the user or group managed by the resource.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>500 internal server error</li>
* </ul>
* Returns all permissions of the user or group managed by the resource.
*
* @return all permissions of the user or group
*/
@GET
@StatusCodes({
@ResponseCode(code = 204, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public List<Permission> getAll()
{

View File

@@ -39,7 +39,10 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.List;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
@@ -47,8 +50,6 @@ import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.subject.Subject;
import org.codehaus.enunciate.jaxrs.TypeHint;
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -59,6 +60,11 @@ import sonia.scm.api.rest.RestActionResult;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.Tokens;
import sonia.scm.util.HttpUtil;
//~--- JDK imports ------------------------------------------------------------
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -81,12 +87,12 @@ import sonia.scm.security.AccessTokenCookieIssuer;
import sonia.scm.security.Scope;
/**
*
* Authentication related RESTful Web Service endpoint.
*
* @author Sebastian Sdorra
*/
@Singleton
@Path("auth")
@ExternallyManagedLifecycle
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public class AuthenticationResource
{
@@ -128,15 +134,8 @@ public class AuthenticationResource
//~--- methods --------------------------------------------------------------
/**
* Authenticate a user and return the state of the application.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>400 bad request, required parameter is missing.</li>
* <li>401 unauthorized, the specified username or password is wrong</li>
* <li>500 internal server error</li>
* </ul>
*
* Authenticate a user and return the state of the application.
*
* @param request current http request
* @param response current http response
* @param grantType grant type, currently only password is supported
@@ -150,6 +149,12 @@ public class AuthenticationResource
@POST
@Path("access_token")
@TypeHint(ScmState.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, required parameter is missing"),
@ResponseCode(code = 401, condition = "unauthorized, the specified username or password is wrong"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response authenticate(
@Context HttpServletRequest request,
@Context HttpServletResponse response,
@@ -238,13 +243,7 @@ public class AuthenticationResource
}
/**
* Logout the current user. Returns the current state of the application,
* if public access is enabled.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>500 internal server error</li>
* </ul>
* Logout the current user. Returns the current state of the application, if public access is enabled.
*
* @param request the current http request
* @param response the current http response
@@ -254,6 +253,10 @@ public class AuthenticationResource
@GET
@Path("logout")
@TypeHint(ScmState.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response logout(@Context HttpServletRequest request, @Context HttpServletResponse response)
{
Subject subject = SecurityUtils.getSubject();
@@ -280,16 +283,8 @@ public class AuthenticationResource
//~--- get methods ----------------------------------------------------------
/**
* This method is an alias of the
* {@link #getState(javax.servlet.http.HttpServletRequest)} method.
* The only difference between the methods,
* is that this one could not be used with basic authentication.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>401 unauthorized, user is not authenticated and public access is disabled.</li>
* <li>500 internal server error</li>
* </ul>
* This method is an alias of the {@link #getState(HttpServletRequest)} method.
* The only difference between the methods, is that this one could not be used with basic authentication.
*
* @param request the current http request
*
@@ -298,19 +293,18 @@ public class AuthenticationResource
@GET
@Path("state")
@TypeHint(ScmState.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 401, condition = "unauthorized, user is not authenticated and public access is disabled"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response getCurrentState(@Context HttpServletRequest request)
{
return getState(request);
}
/**
* Returns the current state of the application.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>401 unauthorized, user is not authenticated and public access is disabled.</li>
* <li>500 internal server error</li>
* </ul>
* Returns the current state of the application.
*
* @param request the current http request
*
@@ -318,6 +312,11 @@ public class AuthenticationResource
*/
@GET
@TypeHint(ScmState.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 401, condition = "unauthorized, user is not authenticated and public access is disabled"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response getState(@Context HttpServletRequest request)
{
Response response;

View File

@@ -36,14 +36,14 @@ package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.PasswordService;
import org.apache.shiro.subject.Subject;
import org.codehaus.enunciate.jaxrs.TypeHint;
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -68,10 +68,10 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
/**
*
* Resource to change the password of the authenticated user.
*
* @author Sebastian Sdorra
*/
@ExternallyManagedLifecycle
@Path("action/change-password")
public class ChangePasswordResource
{
@@ -100,14 +100,7 @@ public class ChangePasswordResource
//~--- methods --------------------------------------------------------------
/**
* Changes the password of the current user.<br />
* <br />
* Status codes:
* <ul>
* <li>200 success</li>
* <li>400 bad request, the old password is not correct</li>
* <li>500 internal server error</li>
* </ul>
* Changes the password of the current user.
*
* @param oldPassword old password of the current user
* @param newPassword new password for the current user
@@ -119,6 +112,11 @@ public class ChangePasswordResource
*/
@POST
@TypeHint(RestActionResult.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, the old password is not correct"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response changePassword(@FormParam("old-password") String oldPassword,
@FormParam("new-password") String newPassword)

View File

@@ -35,6 +35,8 @@ package sonia.scm.api.rest.resources;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import org.apache.shiro.SecurityUtils;
@@ -60,12 +62,7 @@ public class CipherResource
/**
* Encrypts the request body and returns an encrypted string. This method can
* only executed with administration privileges.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>500 internal server error</li>
* </ul>
* only executed with administration privileges.
*
* @param value value to encrypt
*
@@ -73,6 +70,10 @@ public class CipherResource
*/
@POST
@Path("encrypt")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(MediaType.TEXT_PLAIN)
public String encrypt(String value)
{

View File

@@ -41,8 +41,6 @@ import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.Role;
import sonia.scm.security.ScmSecurityException;
@@ -66,7 +64,6 @@ import javax.ws.rs.core.UriInfo;
*/
@Singleton
@Path("config")
@ExternallyManagedLifecycle
public class ConfigurationResource
{

View File

@@ -37,12 +37,13 @@ package sonia.scm.api.rest.resources;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import org.codehaus.enunciate.jaxrs.TypeHint;
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
import sonia.scm.group.Group;
import sonia.scm.group.GroupException;
import sonia.scm.group.GroupManager;
@@ -70,12 +71,12 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
/**
*
* RESTful Web Service Resource to manage groups and their members.
*
* @author Sebastian Sdorra
*/
@Path("groups")
@Singleton
@ExternallyManagedLifecycle
public class GroupResource
extends AbstractManagerResource<Group, GroupException>
{
@@ -102,15 +103,7 @@ public class GroupResource
//~--- methods --------------------------------------------------------------
/**
* Creates a new group.<br />
* This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>201 create success</li>
* <li>403 forbidden, the current user has no admin privileges</li>
* <li>500 internal server error</li>
* </ul>
* Creates a new group. <strong>Note:</strong> This method requires admin privileges.
*
* @param uriInfo current uri informations
* @param group the group to be created
@@ -118,6 +111,14 @@ public class GroupResource
* @return
*/
@POST
@StatusCodes({
@ResponseCode(code = 201, condition = "create success", additionalHeaders = {
@ResponseHeader(name = "Location", description = "uri to the created group")
}),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Override
public Response create(@Context UriInfo uriInfo, Group group)
@@ -126,15 +127,7 @@ public class GroupResource
}
/**
* Deletes a group.<br />
* This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>201 delete success</li>
* <li>403 forbidden, the current user has no admin privileges</li>
* <li>500 internal server error</li>
* </ul>
* Deletes a group. <strong>Note:</strong> This method requires admin privileges.
*
* @param name the name of the group to delete.
*
@@ -142,6 +135,12 @@ public class GroupResource
*/
@DELETE
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "delete success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Override
public Response delete(@PathParam("id") String name)
{
@@ -149,15 +148,7 @@ public class GroupResource
}
/**
* Modifies the given group.<br />
* This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>201 update successful</li>
* <li>403 forbidden, the current user has no admin privileges</li>
* <li>500 internal server error</li>
* </ul>
* Modifies the given group. <strong>Note:</strong> This method requires admin privileges.
*
* @param uriInfo current uri informations
* @param name name of the group to be modified
@@ -167,6 +158,12 @@ public class GroupResource
*/
@PUT
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Override
public Response update(@Context UriInfo uriInfo,
@@ -178,16 +175,7 @@ public class GroupResource
//~--- get methods ----------------------------------------------------------
/**
* Returns a group.<br />
* This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>403 forbidden, the current user has no admin privileges</li>
* <li>404 not found, no group with the specified id/name available</li>
* <li>500 internal server error</li>
* </ul>
* Fetches a group by its name or id. <strong>Note:</strong> This method requires admin privileges.
*
* @param request the current request
* @param id the id/name of the group
@@ -197,6 +185,12 @@ public class GroupResource
@GET
@Path("{id}")
@TypeHint(Group.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Override
public Response get(@Context Request request, @PathParam("id") String id)
@@ -216,15 +210,7 @@ public class GroupResource
}
/**
* Returns all groups.<br />
* This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>403 forbidden, the current user has no admin privileges</li>
* <li>500 internal server error</li>
* </ul>
* Returns all groups. <strong>Note:</strong> This method requires admin privileges.
*
* @param request the current request
* @param start the start value for paging
@@ -237,6 +223,11 @@ public class GroupResource
@GET
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@TypeHint(Group[].class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Override
public Response getAll(@Context Request request, @DefaultValue("0")
@QueryParam("start") int start, @DefaultValue("-1")
@@ -249,14 +240,6 @@ public class GroupResource
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param items
*
* @return
*/
@Override
protected GenericEntity<Collection<Group>> createGenericEntity(
Collection<Group> items)
@@ -267,26 +250,12 @@ public class GroupResource
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param group
*
* @return
*/
@Override
protected String getId(Group group)
{
return group.getName();
}
/**
* Method description
*
*
* @return
*/
@Override
protected String getPathPart()
{

View File

@@ -34,11 +34,11 @@ package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import org.apache.shiro.SecurityUtils;
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
import sonia.scm.security.KeyGenerator;
import sonia.scm.security.Role;
@@ -56,7 +56,6 @@ import javax.ws.rs.core.MediaType;
* @since 1.41
*/
@Path("security/key")
@ExternallyManagedLifecycle
public class KeyResource
{
@@ -75,17 +74,15 @@ public class KeyResource
//~--- methods --------------------------------------------------------------
/**
* Generates a unique key. This method can only executed with administration
* privileges.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>500 internal server error</li>
* </ul>
* Generates a unique key. <strong>Note:</strong> This method can only executed with administration privileges.
*
* @return unique key
*/
@GET
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(MediaType.TEXT_PLAIN)
public String generateKey()
{

View File

@@ -39,8 +39,6 @@ import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -55,6 +53,9 @@ import sonia.scm.plugin.PluginManager;
//~--- JDK imports ------------------------------------------------------------
import com.sun.jersey.multipart.FormDataParam;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import java.io.IOException;
import java.io.InputStream;
@@ -75,12 +76,12 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
/**
*
* RESTful Web Service Endpoint to manage plugins.
*
* @author Sebastian Sdorra
*/
@Singleton
@Path("plugins")
@ExternallyManagedLifecycle
public class PluginResource
{
@@ -107,21 +108,21 @@ public class PluginResource
//~--- methods --------------------------------------------------------------
/**
* Installs a plugin from a package.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>412 precondition failed</li>
* <li>500 internal server error</li>
* </ul>
* Installs a plugin from a package.
*
* @param uploadedInputStream
*
* @return
*
* @throws IOException
*/
@POST
@Path("install-package")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 412, condition = "precondition failed"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response install(
@@ -153,35 +154,30 @@ public class PluginResource
}
/**
* Installs a plugin.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>500 internal server error</li>
* </ul>
* Installs a plugin.
*
* @param id id of the plugin to be installed
*
* @return
*/
@POST
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Path("install/{id}")
public Response install(@PathParam("id") String id)
{
pluginManager.install(id);
// TODO should return 204 no content
return Response.ok().build();
}
/**
* Installs a plugin from a package. This method is a workaround for ExtJS
* file upload, which requires text/html as content-type.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>412 precondition failed</li>
* <li>500 internal server error</li>
* </ul>
* file upload, which requires text/html as content-type.
*
* @param uploadedInputStream
* @return
@@ -190,6 +186,11 @@ public class PluginResource
*/
@POST
@Path("install-package.html")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 412, condition = "precondition failed"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_HTML)
public Response installFromUI(
@@ -200,60 +201,62 @@ public class PluginResource
}
/**
* Uninstalls a plugin.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>500 internal server error</li>
* </ul>
* Uninstalls a plugin.
*
* @param id id of the plugin to be uninstalled
*
* @return
*/
@POST
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Path("uninstall/{id}")
public Response uninstall(@PathParam("id") String id)
{
pluginManager.uninstall(id);
// TODO should return 204 content
// consider to do a uninstall with a delete
return Response.ok().build();
}
/**
* Updates a plugin.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>500 internal server error</li>
* </ul>
* Updates a plugin.
*
* @param id id of the plugin to be updated
*
* @return
*/
@POST
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Path("update/{id}")
public Response update(@PathParam("id") String id)
{
pluginManager.update(id);
// TODO should return 204 content
// consider to do an update with a put
return Response.ok().build();
}
//~--- get methods ----------------------------------------------------------
/**
* Returns all plugins.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>500 internal server error</li>
* </ul>
* Returns all plugins.
*
* @return all plugins
*/
@GET
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Collection<PluginInformation> getAll()
{
@@ -261,17 +264,16 @@ public class PluginResource
}
/**
* Returns all available plugins.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>500 internal server error</li>
* </ul>
* Returns all available plugins.
*
* @return all available plugins
*/
@GET
@Path("available")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Collection<PluginInformation> getAvailable()
{
@@ -279,17 +281,16 @@ public class PluginResource
}
/**
* Returns all plugins which are available for update.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>500 internal server error</li>
* </ul>
* Returns all plugins which are available for update.
*
* @return all plugins which are available for update
*/
@GET
@Path("updates")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Collection<PluginInformation> getAvailableUpdates()
{
@@ -297,17 +298,16 @@ public class PluginResource
}
/**
* Returns all installed plugins.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>500 internal server error</li>
* </ul>
* Returns all installed plugins.
*
* @return all installed plugins
*/
@GET
@Path("installed")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Collection<PluginInformation> getInstalled()
{
@@ -315,17 +315,16 @@ public class PluginResource
}
/**
* Returns all plugins for the overview.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>500 internal server error</li>
* </ul>
* Returns all plugins for the overview.
*
* @return all plugins for the overview
*/
@GET
@Path("overview")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Collection<PluginInformation> getOverview()
{

View File

@@ -43,9 +43,6 @@ import com.google.inject.Inject;
import org.apache.shiro.SecurityUtils;
import org.codehaus.enunciate.jaxrs.TypeHint;
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -74,6 +71,10 @@ import static com.google.common.base.Preconditions.*;
import com.sun.jersey.api.client.ClientResponse.Status;
import com.sun.jersey.multipart.FormDataParam;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import java.io.File;
import java.io.IOException;
@@ -111,7 +112,6 @@ import javax.xml.bind.annotation.XmlRootElement;
* @author Sebastian Sdorra
*/
@Path("import/repositories")
@ExternallyManagedLifecycle
public class RepositoryImportResource
{
@@ -142,17 +142,8 @@ public class RepositoryImportResource
/**
* Imports a repository type specific bundle. The bundle file is uploaded to
* the server which is running scm-manager. After the upload has finished, the
* bundle file is passed to the {@link UnbundleCommandBuilder}. This method
* requires admin privileges.<br />
*
* Status codes:
* <ul>
* <li>201 created</li>
* <li>400 bad request, the import bundle feature is not supported by this
* type of repositories or the parameters are not valid.</li>
* <li>500 internal server error</li>
* <li>409 conflict, a repository with the name already exists.</li>
* </ul>
* bundle file is passed to the {@link UnbundleCommandBuilder}. <strong>Note:</strong> This method
* requires admin privileges.
*
* @param uriInfo uri info
* @param type repository type
@@ -160,12 +151,23 @@ public class RepositoryImportResource
* @param inputStream input bundle
* @param compressed true if the bundle is gzip compressed
*
* @return empty response with location header which points to the imported
* repository
* @return empty response with location header which points to the imported repository
* @since 1.43
*/
@POST
@Path("{type}/bundle")
@StatusCodes({
@ResponseCode(code = 201, condition = "created", additionalHeaders = {
@ResponseHeader(name = "Location", description = "uri to the imported repository")
}),
@ResponseCode(
code = 400,
condition = "bad request, the import bundle feature is not supported by this type of repositories or the parameters are not valid"
),
@ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response importFromBundle(@Context UriInfo uriInfo,
@PathParam("type") String type, @FormDataParam("name") String name,
@@ -182,18 +184,8 @@ public class RepositoryImportResource
* This method works exactly like
* {@link #importFromBundle(UriInfo, String, String, InputStream)}, but this
* method returns an html content-type. The method exists only for a
* workaround of the javascript ui extjs. This method requires admin
* privileges.<br />
*
* Status codes:
* <ul>
* <li>201 created</li>
* <li>400 bad request, the import bundle feature is not supported by this
* type of repositories or the parameters are not valid.</li>
* <li>500 internal server error</li>
* <li>409 conflict, a repository with the name already exists.</li>
* </ul>
*
* workaround of the javascript ui extjs. <strong>Note:</strong> This method requires admin
* privileges.
*
* @param type repository type
* @param name name of the repository
@@ -206,6 +198,16 @@ public class RepositoryImportResource
*/
@POST
@Path("{type}/bundle.html")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(
code = 400,
condition = "bad request, the import bundle feature is not supported by this type of repositories or the parameters are not valid"
),
@ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(RestActionUploadResult.class)
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_HTML)
public Response importFromBundleUI(@PathParam("type") String type,
@@ -234,16 +236,7 @@ public class RepositoryImportResource
* Imports a external repository which is accessible via url. The method can
* only be used, if the repository type supports the {@link Command#PULL}. The
* method will return a location header with the url to the imported
* repository. This method requires admin privileges.<br />
*
* Status codes:
* <ul>
* <li>201 created</li>
* <li>400 bad request, the import by url feature is not supported by this
* type of repositories or the parameters are not valid.</li>
* <li>409 conflict, a repository with the name already exists.</li>
* <li>500 internal server error</li>
* </ul>
* repository. <strong>Note:</strong> This method requires admin privileges.
*
* @param uriInfo uri info
* @param type repository type
@@ -255,6 +248,18 @@ public class RepositoryImportResource
*/
@POST
@Path("{type}/url")
@StatusCodes({
@ResponseCode(code = 201, condition = "created", additionalHeaders = {
@ResponseHeader(name = "Location", description = "uri to the imported repository")
}),
@ResponseCode(
code = 400,
condition = "bad request, the import feature is not supported by this type of repositories or the parameters are not valid"
),
@ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response importFromUrl(@Context UriInfo uriInfo,
@PathParam("type") String type, UrlImportRequest request)
@@ -298,15 +303,7 @@ public class RepositoryImportResource
/**
* Imports repositories of the given type from the configured repository
* directory. This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>200 ok, successful</li>
* <li>400 bad request, the import feature is not
* supported by this type of repositories.</li>
* <li>500 internal server error</li>
* </ul>
* directory. <strong>Note:</strong> This method requires admin privileges.
*
* @param type repository type
*
@@ -314,6 +311,14 @@ public class RepositoryImportResource
*/
@POST
@Path("{type}")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(
code = 400,
condition = "bad request, the import feature is not supported by this type of repositories"
),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(Repository[].class)
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response importRepositories(@PathParam("type") String type)
@@ -333,19 +338,19 @@ public class RepositoryImportResource
/**
* Imports repositories of all supported types from the configured repository
* directories. This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>200 ok, successful</li>
* <li>400 bad request, the import feature is not
* supported by this type of repositories.</li>
* <li>500 internal server error</li>
* </ul>
* directories. <strong>Note:</strong> This method requires admin privileges.
*
* @return imported repositories
*/
@POST
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(
code = 400,
condition = "bad request, the import feature is not supported by this type of repositories"
),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(Repository[].class)
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response importRepositories()
@@ -371,15 +376,7 @@ public class RepositoryImportResource
/**
* Imports repositories of the given type from the configured repository
* directory. Returns a list of successfully imported directories and a list
* of failed directories. This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>200 ok, successful</li>
* <li>400 bad request, the import feature is not
* supported by this type of repositories.</li>
* <li>500 internal server error</li>
* </ul>
* of failed directories. <strong>Note:</strong> This method requires admin privileges.
*
* @param type repository type
*
@@ -388,6 +385,14 @@ public class RepositoryImportResource
*/
@POST
@Path("{type}/directory")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(
code = 400,
condition = "bad request, the import feature is not supported by this type of repositories"
),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(ImportResult.class)
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response importRepositoriesFromDirectory(
@@ -456,22 +461,20 @@ public class RepositoryImportResource
/**
* Returns a list of repository types, which support the directory import
* feature.
*
* This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>200 ok, successful</li>
* <li>400 bad request, the import feature is not
* supported by this type of repositories.</li>
* <li>500 internal server error</li>
* </ul>
* feature. <strong>Note:</strong> This method requires admin privileges.
*
* @return list of repository types
*/
@GET
@TypeHint(Type[].class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(
code = 400,
condition = "bad request, the import feature is not supported by this type of repositories"
),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response getImportableTypes()
{

View File

@@ -38,12 +38,13 @@ package sonia.scm.api.rest.resources;
import com.google.common.base.Strings;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import org.codehaus.enunciate.jaxrs.TypeHint;
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -103,14 +104,13 @@ import javax.ws.rs.core.UriInfo;
import org.apache.shiro.authz.AuthorizationException;
/**
*
* Repository related RESTful Web Service Endpoint.
*
* @author Sebastian Sdorra
*/
@Singleton
@Path("repositories")
@ExternallyManagedLifecycle
public class RepositoryResource
extends AbstractManagerResource<Repository, RepositoryException>
public class RepositoryResource extends AbstractManagerResource<Repository, RepositoryException>
{
/** Field description */
@@ -147,22 +147,22 @@ public class RepositoryResource
//~--- methods --------------------------------------------------------------
/**
* Creates a new repository.<br />
* This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>201 create success</li>
* <li>403 forbidden, the current user has no admin privileges</li>
* <li>500 internal server error</li>
* </ul>
* Creates a new repository.<strong>Note:</strong> This method requires admin privileges.
*
* @param uriInfo current uri informations
* @param repository the repository to be created
*
* @return
* @return empty response with location header to the new repository
*/
@POST
@StatusCodes({
@ResponseCode(code = 201, condition = "success", additionalHeaders = {
@ResponseHeader(name = "Location", description = "uri to the new created repository")
}),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Override
public Response create(@Context UriInfo uriInfo, Repository repository)
@@ -171,19 +171,7 @@ public class RepositoryResource
}
/**
* Deletes a repository.<br />
* This method requires owner privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>201 delete success</li>
* <li>403 forbidden, the current user has no owner privileges</li>
* <li>
* 412 forbidden, the repository is not archived,
* this error occurs only with enabled repository archive.
* </li>
* <li>500 internal server error</li>
* </ul>
* Deletes a repository. <strong>Note:</strong> This method requires owner privileges.
*
* @param id the id of the repository to delete.
*
@@ -191,6 +179,17 @@ public class RepositoryResource
*/
@DELETE
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "delete success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no owner privileges"),
@ResponseCode(code = 404, condition = "could not find repository"),
@ResponseCode(
code = 412,
condition = "precondition failed, the repository is not archived, this error occurs only with enabled repository archive"
),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Override
public Response delete(@PathParam("id") String id)
{
@@ -232,20 +231,20 @@ public class RepositoryResource
}
/**
* Re run repository health checks.<br />
* Status codes:
* <ul>
* <li>201 re run success</li>
* <li>403 forbidden, the current user has no owner privileges</li>
* <li>404 could not find repository</li>
* <li>500 internal server error</li>
* </ul>
* Re run repository health checks.
*
* @param id id of the repository
*
* @return
*/
@POST
@StatusCodes({
@ResponseCode(code = 200, condition = "re run success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no owner privileges"),
@ResponseCode(code = 404, condition = "could not find repository"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Path("{id}/healthcheck")
public Response runHealthChecks(@PathParam("id") String id)
{
@@ -254,6 +253,7 @@ public class RepositoryResource
try
{
healthChecker.check(id);
// TODO should return 204 instead of 200
response = Response.ok().build();
}
catch (RepositoryNotFoundException ex)
@@ -271,15 +271,7 @@ public class RepositoryResource
}
/**
* Modifies the given repository.<br />
* This method requires owner privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>201 update successful</li>
* <li>403 forbidden, the current user has no owner privileges</li>
* <li>500 internal server error</li>
* </ul>
* Modifies the given repository. <strong>Note:</strong> This method requires owner privileges.
*
* @param uriInfo current uri informations
* @param id id of the repository to be modified
@@ -289,10 +281,16 @@ public class RepositoryResource
*/
@PUT
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "update successful"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no owner privileges"),
@ResponseCode(code = 404, condition = "could not find repository"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Override
public Response update(@Context UriInfo uriInfo, @PathParam("id") String id,
Repository repository)
public Response update(@Context UriInfo uriInfo, @PathParam("id") String id, Repository repository)
{
return super.update(uriInfo, id, repository);
}
@@ -300,14 +298,7 @@ public class RepositoryResource
//~--- get methods ----------------------------------------------------------
/**
* Returns the {@link Repository} with the specified id.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>404 not found, no repository with the specified id available</li>
* <li>500 internal server error</li>
* </ul>
* Returns the {@link Repository} with the specified id.
*
* @param request the current request
* @param id the id/name of the user
@@ -317,6 +308,11 @@ public class RepositoryResource
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 404, condition = "not found, no repository with the specified id available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(Repository.class)
@Override
public Response get(@Context Request request, @PathParam("id") String id)
@@ -325,13 +321,7 @@ public class RepositoryResource
}
/**
* Returns all repositories.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>500 internal server error</li>
* </ul>
* Returns all repositories.
*
* @param request the current request
* @param start the start value for paging
@@ -343,6 +333,10 @@ public class RepositoryResource
*/
@GET
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(Repository[].class)
@Override
public Response getAll(@Context Request request, @DefaultValue("0")
@@ -355,16 +349,7 @@ public class RepositoryResource
}
/**
* Returns a annotate/blame view for the given path.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>400 bad request, the blame feature is not
* supported by this type of repositories.</li>
* <li>404 not found, if the repository or the path could not be found</li>
* <li>500 internal server error</li>
* </ul>
* Returns a annotate/blame view for the given path.
*
* @param id the id of the repository
* @param revision the revision of the file
@@ -377,6 +362,12 @@ public class RepositoryResource
*/
@GET
@Path("{id}/blame")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, the blame feature is not supported by this type of repositories."),
@ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(BlameResult.class)
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response getBlame(@PathParam("id") String id,
@@ -430,16 +421,7 @@ public class RepositoryResource
}
/**
* Returns all {@link Branches} of a repository.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>400 bad request, the content feature is not
* supported by this type of repositories.</li>
* <li>404 not found, if the repository or the path could not be found</li>
* <li>500 internal server error</li>
* </ul>
* Returns all {@link Branches} of a repository.
*
* @param id the id of the repository
*
@@ -452,6 +434,14 @@ public class RepositoryResource
*/
@GET
@Path("{id}/branches")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, the branch feature is not supported by this type of repositories."),
@ResponseCode(code = 404, condition = "not found, the repository could not be found"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(Branches.class)
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response getBranches(@PathParam("id") String id)
throws RepositoryException, IOException
{
@@ -490,16 +480,7 @@ public class RepositoryResource
}
/**
* Returns a list of folders and files for the given folder.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>400 bad request, the browse feature is not
* supported by this type of repositories.</li>
* <li>404 not found, if the repository or the path could not be found</li>
* <li>500 internal server error</li>
* </ul>
* Returns a list of folders and files for the given folder.
*
* @param id the id of the repository
* @param revision the revision of the file
@@ -515,6 +496,12 @@ public class RepositoryResource
*/
@GET
@Path("{id}/browse")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, the browse feature is not supported by this type of repositories."),
@ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(BrowserResult.class)
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
//J-
@@ -581,15 +568,7 @@ public class RepositoryResource
}
/**
* Returns the {@link Repository} with the specified type and name.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>404 not found,
* no repository with the specified type and name available</li>
* <li>500 internal server error</li>
* </ul>
* Returns the {@link Repository} with the specified type and name.
*
* @param type the type of the repository
* @param name the name of the repository
@@ -598,8 +577,13 @@ public class RepositoryResource
*/
@GET
@Path("{type: [a-z]+}/{name: .*}")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 404, condition = "not found, no repository with the specified type and name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(Repository.class)
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response getByTypeAndName(@PathParam("type") String type,
@PathParam("name") String name)
{
@@ -621,17 +605,7 @@ public class RepositoryResource
/**
* Returns the {@link Changeset} from the given repository
* with the specified revision.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>400 bad request, the changeset feature is not
* supported by this type of repositories.</li>
* <li>404 not found, if the repository or
* the revision could not be found</li>
* <li>500 internal server error</li>
* </ul>
* with the specified revision.
*
* @param id the id of the repository
* @param revision the revision of the changeset
@@ -643,6 +617,14 @@ public class RepositoryResource
*/
@GET
@Path("{id}/changeset/{revision}")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, the changeset feature is not supported by this type of repositories."),
@ResponseCode(code = 404, condition = "not found, the repository or the revision could not be found"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(Changeset.class)
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response getChangeset(@PathParam("id") String id,
@PathParam("revision") String revision)
throws IOException, RepositoryException
@@ -691,16 +673,7 @@ public class RepositoryResource
}
/**
* Returns a list of {@link Changeset} for the given repository.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>400 bad request, the changeset feature is not
* supported by this type of repositories.</li>
* <li>404 not found, if the repository or the path could not be found</li>
* <li>500 internal server error</li>
* </ul>
* Returns a list of {@link Changeset} for the given repository.
*
* @param id the id of the repository
* @param path path of a file
@@ -716,6 +689,12 @@ public class RepositoryResource
*/
@GET
@Path("{id}/changesets")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, the changeset feature is not supported by this type of repositories."),
@ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(ChangesetPagingResult.class)
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
//J-
@@ -784,16 +763,7 @@ public class RepositoryResource
}
/**
* Returns the content of a file.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>400 bad request, the content feature is not
* supported by this type of repositories.</li>
* <li>404 not found, if the repository or the path could not be found</li>
* <li>500 internal server error</li>
* </ul>
* Returns the content of a file.
*
* @param id the id of the repository
* @param revision the revision of the file
@@ -803,6 +773,12 @@ public class RepositoryResource
*/
@GET
@Path("{id}/content")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, the content feature is not supported by this type of repositories."),
@ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(StreamingOutput.class)
@Produces({ MediaType.APPLICATION_OCTET_STREAM })
public Response getContent(@PathParam("id") String id,
@@ -855,16 +831,7 @@ public class RepositoryResource
}
/**
* Returns the modifications of a {@link Changeset}.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>400 bad request, the content feature is not
* supported by this type of repositories.</li>
* <li>404 not found, if the repository or the path could not be found</li>
* <li>500 internal server error</li>
* </ul>
* Returns the modifications of a {@link Changeset}.
*
* @param id the id of the repository
* @param revision the revision of the file
@@ -878,6 +845,12 @@ public class RepositoryResource
*/
@GET
@Path("{id}/diff")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, the diff feature is not supported by this type of repositories."),
@ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(DiffStreamingOutput.class)
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response getDiff(@PathParam("id") String id,
@@ -943,16 +916,7 @@ public class RepositoryResource
}
/**
* Returns all {@link Tags} of a repository.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>400 bad request, the content feature is not
* supported by this type of repositories.</li>
* <li>404 not found, if the repository or the path could not be found</li>
* <li>500 internal server error</li>
* </ul>
* Returns all {@link Tags} of a repository.
*
* @param id the id of the repository
*
@@ -965,6 +929,14 @@ public class RepositoryResource
*/
@GET
@Path("{id}/tags")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, the tag feature is not supported by this type of repositories."),
@ResponseCode(code = 404, condition = "not found, the repository could not be found"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(Tags.class)
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response getTags(@PathParam("id") String id)
throws RepositoryException, IOException
{

View File

@@ -41,8 +41,6 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.inject.Inject;
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryTypePredicate;
@@ -74,7 +72,6 @@ import javax.ws.rs.core.MediaType;
*
* @author Sebastian Sdorra
*/
@ExternallyManagedLifecycle
@Path("help/repository-root/{type}.html")
public class RepositoryRootResource
{

View File

@@ -40,8 +40,8 @@ import com.github.legman.Subscribe;
import com.google.common.base.Function;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
@@ -64,12 +64,13 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
/**
*
* RESTful Web Service Resource to search users and groups. This endpoint can be used to implement typeahead input
* fields for permissions.
*
* @author Sebastian Sdorra
*/
@Singleton
@Path("search")
@ExternallyManagedLifecycle
public class SearchResource
{
@@ -140,12 +141,7 @@ public class SearchResource
}
/**
* Returns a list of groups found by the given search string.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>500 internal server error</li>
* </ul>
* Returns a list of groups found by the given search string.
*
* @param queryString the search string
*
@@ -153,6 +149,10 @@ public class SearchResource
*/
@GET
@Path("groups")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public SearchResults searchGroups(@QueryParam("query") String queryString)
{
@@ -176,12 +176,7 @@ public class SearchResource
}
/**
* Returns a list of users found by the given search string.<br />
* <br />
* <ul>
* <li>200 success</li>
* <li>500 internal server error</li>
* </ul>
* Returns a list of users found by the given search string.
*
* @param queryString the search string
*
@@ -189,6 +184,10 @@ public class SearchResource
*/
@GET
@Path("users")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public SearchResults searchUsers(@QueryParam("query") String queryString)
{

View File

@@ -45,14 +45,13 @@ import sonia.scm.security.SecuritySystem;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
/**
*
* Resource for managing system security permissions.
*
* @author Sebastian Sdorra
*/
@Path("security/permission")
@ExternallyManagedLifecycle
public class SecuritySystemResource
{
@@ -74,31 +73,28 @@ public class SecuritySystemResource
//~--- get methods ----------------------------------------------------------
/**
* Method description
* Returns group permission sub resource.
*
* @param group name of group
*
* @param group
*
* @return
* @return sub resource
*/
@Path("group/{group}")
public GroupPermissionResource getGroupSubResource(
@PathParam("group") String group)
public GroupPermissionResource getGroupSubResource(@PathParam("group") String group)
{
return new GroupPermissionResource(system, group);
}
/**
* Method description
* Returns user permission sub resource.
*
*
* @param user
* @param user name of user
*
* @return
* @return sub resource
*/
@Path("user/{user}")
public UserPermissionResource getUserSubResource(
@PathParam("user") String user)
public UserPermissionResource getUserSubResource(@PathParam("user") String user)
{
return new UserPermissionResource(system, user);
}
@@ -106,5 +102,5 @@ public class SecuritySystemResource
//~--- fields ---------------------------------------------------------------
/** Field description */
private SecuritySystem system;
private final SecuritySystem system;
}

View File

@@ -42,8 +42,6 @@ import com.google.inject.Inject;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
import sonia.scm.SCMContextProvider;
import sonia.scm.ServletContainerDetector;
import sonia.scm.Type;
@@ -79,7 +77,6 @@ import sonia.scm.store.ConfigurationStoreFactory;
* @author Sebastian Sdorra
*/
@Path("support")
@ExternallyManagedLifecycle
public class SupportResource
{

View File

@@ -37,13 +37,14 @@ package sonia.scm.api.rest.resources;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.PasswordService;
import org.codehaus.enunciate.jaxrs.TypeHint;
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
import sonia.scm.security.Role;
import sonia.scm.user.User;
import sonia.scm.user.UserException;
@@ -73,12 +74,12 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
/**
*
* RESTful Web Service Resource to manage users.
*
* @author Sebastian Sdorra
*/
@Singleton
@Path("users")
@ExternallyManagedLifecycle
public class UserResource extends AbstractManagerResource<User, UserException>
{
@@ -107,15 +108,7 @@ public class UserResource extends AbstractManagerResource<User, UserException>
//~--- methods --------------------------------------------------------------
/**
* Creates a new user.<br />
* This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>201 create success</li>
* <li>403 forbidden, the current user has no admin privileges</li>
* <li>500 internal server error</li>
* </ul>
* Creates a new user. <strong>Note:</strong> This method requires admin privileges.
*
* @param uriInfo current uri informations
* @param user the user to be created
@@ -123,6 +116,14 @@ public class UserResource extends AbstractManagerResource<User, UserException>
* @return
*/
@POST
@StatusCodes({
@ResponseCode(code = 201, condition = "create success", additionalHeaders = {
@ResponseHeader(name = "Location", description = "uri to the created group")
}),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Override
public Response create(@Context UriInfo uriInfo, User user)
@@ -131,15 +132,7 @@ public class UserResource extends AbstractManagerResource<User, UserException>
}
/**
* Deletes a user.<br />
* This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>201 delete success</li>
* <li>403 forbidden, the current user has no admin privileges</li>
* <li>500 internal server error</li>
* </ul>
* Deletes a user. <strong>Note:</strong> This method requires admin privileges.
*
* @param name the name of the user to delete.
*
@@ -147,6 +140,12 @@ public class UserResource extends AbstractManagerResource<User, UserException>
*/
@DELETE
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "delete success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Override
public Response delete(@PathParam("id") String name)
{
@@ -154,15 +153,7 @@ public class UserResource extends AbstractManagerResource<User, UserException>
}
/**
* Modifies the given user.<br />
* This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>201 update successful</li>
* <li>403 forbidden, the current user has no admin privileges</li>
* <li>500 internal server error</li>
* </ul>
* Modifies the given user. <strong>Note:</strong> This method requires admin privileges.
*
* @param uriInfo current uri informations
* @param name name of the user to be modified
@@ -172,6 +163,12 @@ public class UserResource extends AbstractManagerResource<User, UserException>
*/
@PUT
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Override
public Response update(@Context UriInfo uriInfo,
@@ -183,16 +180,7 @@ public class UserResource extends AbstractManagerResource<User, UserException>
//~--- get methods ----------------------------------------------------------
/**
* Returns a user.<br />
* This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>403 forbidden, the current user has no admin privileges</li>
* <li>404 not found, no user with the specified id/name available</li>
* <li>500 internal server error</li>
* </ul>
* Returns a user. <strong>Note:</strong> This method requires admin privileges.
*
* @param request the current request
* @param id the id/name of the user
@@ -202,6 +190,12 @@ public class UserResource extends AbstractManagerResource<User, UserException>
@GET
@Path("{id}")
@TypeHint(User.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Override
public Response get(@Context Request request, @PathParam("id") String id)
@@ -221,15 +215,7 @@ public class UserResource extends AbstractManagerResource<User, UserException>
}
/**
* Returns all users.<br />
* This method requires admin privileges.<br />
* <br />
* Status codes:
* <ul>
* <li>200 get successful</li>
* <li>403 forbidden, the current user has no admin privileges</li>
* <li>500 internal server error</li>
* </ul>
* Returns all users. <strong>Note:</strong> This method requires admin privileges.
*
* @param request the current request
* @param start the start value for paging
@@ -241,6 +227,11 @@ public class UserResource extends AbstractManagerResource<User, UserException>
*/
@GET
@TypeHint(User[].class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Override
public Response getAll(@Context Request request, @DefaultValue("0")
@@ -254,14 +245,6 @@ public class UserResource extends AbstractManagerResource<User, UserException>
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param items
*
* @return
*/
@Override
protected GenericEntity<Collection<User>> createGenericEntity(
Collection<User> items)
@@ -270,24 +253,12 @@ public class UserResource extends AbstractManagerResource<User, UserException>
;
}
/**
* Method description
*
*
* @param user
*/
@Override
protected void preCreate(User user)
{
encryptPassword(user);
}
/**
* Method description
*
*
* @param user
*/
@Override
protected void preUpate(User user)
{
@@ -304,14 +275,6 @@ public class UserResource extends AbstractManagerResource<User, UserException>
}
}
/**
* Method description
*
*
* @param users
*
* @return
*/
@Override
protected Collection<User> prepareForReturn(Collection<User> users)
{
@@ -326,14 +289,6 @@ public class UserResource extends AbstractManagerResource<User, UserException>
return users;
}
/**
* Method description
*
*
* @param user
*
* @return
*/
@Override
protected User prepareForReturn(User user)
{
@@ -342,42 +297,18 @@ public class UserResource extends AbstractManagerResource<User, UserException>
return user;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param user
*
* @return
*/
@Override
protected String getId(User user)
{
return user.getName();
}
/**
* Method description
*
*
* @return
*/
@Override
protected String getPathPart()
{
return PATH_PART;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param user
*/
private void encryptPassword(User user)
{
String password = user.getPassword();

View File

@@ -63,8 +63,7 @@ public class GuavaCache<K, V>
/**
* the logger for GuavaCache
*/
private static final Logger logger =
LoggerFactory.getLogger(GuavaCache.class);
private static final Logger logger = LoggerFactory.getLogger(GuavaCache.class);
//~--- constructors ---------------------------------------------------------
@@ -89,8 +88,7 @@ public class GuavaCache<K, V>
@SuppressWarnings("unchecked")
public GuavaCache(GuavaCacheConfiguration configuration, String name)
{
this(GuavaCaches.create(configuration, name),
configuration.getCopyStrategy(), name);
this(GuavaCaches.create(configuration, name), configuration.getCopyStrategy(), name);
}
/**
@@ -117,7 +115,7 @@ public class GuavaCache<K, V>
this.copyStrategy = CopyStrategy.NONE;
}
}
//~--- methods --------------------------------------------------------------
/**

View File

@@ -49,7 +49,8 @@ import java.io.IOException;
import java.util.Map;
/**
*
* Guava based implementation of {@link CacheManager} and {@link org.apache.shiro.cache.CacheManager}.
*
* @author Sebastian Sdorra
*/
@Singleton
@@ -57,7 +58,7 @@ public class GuavaCacheManager
implements CacheManager, org.apache.shiro.cache.CacheManager
{
/**
/**
* the logger for GuavaCacheManager
*/
private static final Logger logger =

View File

@@ -104,15 +104,18 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager
* @param keyGenerator
* @param repositoryDAO
* @param handlerSet
* @param repositoryMatcher
*/
@Inject
public DefaultRepositoryManager(ScmConfiguration configuration,
SCMContextProvider contextProvider, KeyGenerator keyGenerator,
RepositoryDAO repositoryDAO, Set<RepositoryHandler> handlerSet)
RepositoryDAO repositoryDAO, Set<RepositoryHandler> handlerSet,
RepositoryMatcher repositoryMatcher)
{
this.configuration = configuration;
this.keyGenerator = keyGenerator;
this.repositoryDAO = repositoryDAO;
this.repositoryMatcher = repositoryMatcher;
//J-
ThreadFactory factory = new ThreadFactoryBuilder()
@@ -558,7 +561,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager
for (Repository r : repositories)
{
if (type.equals(r.getType()) && isNameMatching(r, uri))
if (repositoryMatcher.matches(r, type, uri))
{
check.check(r);
repository = r.clone();
@@ -715,30 +718,6 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager
return handler;
}
/**
* Method description
*
*
* @param repository
* @param path
*
* @return
*/
private boolean isNameMatching(Repository repository, String path)
{
boolean result = false;
String name = repository.getName();
if (path.startsWith(name))
{
String sub = path.substring(name.length());
result = Util.isEmpty(sub) || sub.startsWith(HttpUtil.SEPARATOR_PATH);
}
return result;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@@ -758,4 +737,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager
/** Field description */
private final Set<Type> types;
/** Field description */
private RepositoryMatcher repositoryMatcher;
}

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.repository;
import com.google.common.collect.Maps;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
/**
* RepositoryMatcher is able to check if a repository matches the requested path.
*
* @author Sebastian Sdorra
* @since 1.54
*/
public final class RepositoryMatcher {
private static final Logger LOG = LoggerFactory.getLogger(RepositoryMatcher.class);
private static final RepositoryPathMatcher DEFAULT_PATH_MATCHER = new DefaultRepositoryPathMatcher();
private final Map<String, RepositoryPathMatcher> pathMatchers;
/**
* Creates a new instance.
*
* @param pathMatchers injected set of {@link RepositoryPathMatcher}.
*/
@Inject
public RepositoryMatcher(Set<RepositoryPathMatcher> pathMatchers) {
this.pathMatchers = Maps.newHashMap();
for ( RepositoryPathMatcher pathMatcher : pathMatchers ) {
LOG.info("register custom repository path matcher for type {}", pathMatcher.getType());
this.pathMatchers.put(pathMatcher.getType(), pathMatcher);
}
}
/**
* Returns {@code true} is the repository matches the type and the name matches the requested path.
*
* @param repository repository
* @param type type of repository
* @param path requested path without context and without type information
*
* @return {@code true} is the repository matches
*/
public boolean matches(Repository repository, String type, String path) {
return type.equals(repository.getType()) && isPathMatching(repository, path);
}
private boolean isPathMatching(Repository repository, String path) {
return getPathMatcherForType(repository.getType()).isPathMatching(repository, path);
}
private RepositoryPathMatcher getPathMatcherForType(String type) {
RepositoryPathMatcher pathMatcher = pathMatchers.get(type);
if (pathMatcher == null) {
pathMatcher = DEFAULT_PATH_MATCHER;
}
return pathMatcher;
}
private static class DefaultRepositoryPathMatcher implements RepositoryPathMatcher {
@Override
public boolean isPathMatching(Repository repository, String path) {
String name = repository.getName();
if (path.startsWith(name)) {
String sub = path.substring(name.length());
return Util.isEmpty(sub) || sub.startsWith(HttpUtil.SEPARATOR_PATH);
}
return false;
}
@Override
public String getType() {
return "any";
}
}
}

View File

@@ -0,0 +1,271 @@
/**
* Copyright (c) 2014, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.security;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.eventbus.Subscribe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.EagerSingleton;
import sonia.scm.ModificationHandlerEvent;
import sonia.scm.event.HandlerEvent;
import sonia.scm.event.ScmEventBus;
import sonia.scm.group.Group;
import sonia.scm.group.GroupEvent;
import sonia.scm.group.GroupModificationEvent;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryEvent;
import sonia.scm.repository.RepositoryModificationEvent;
import sonia.scm.user.User;
import sonia.scm.user.UserEvent;
import sonia.scm.user.UserModificationEvent;
/**
* Receives all kinds of events, which affects authorization relevant data and fires an
* {@link AuthorizationChangedEvent} if authorization data has changed.
*
* @author Sebastian Sdorra
* @since 1.52
*/
@EagerSingleton
public class AuthorizationChangedEventProducer {
/**
* the logger for AuthorizationChangedEventProducer
*/
private static final Logger logger = LoggerFactory.getLogger(AuthorizationChangedEventProducer.class);
/**
* Constructs a new instance.
*/
public AuthorizationChangedEventProducer() {
}
/**
* Invalidates the cache of a user which was modified. The cache entries for the user will be invalidated for the
* following reasons:
* <ul>
* <li>Admin or Active flag was modified.</li>
* <li>New user created, for the case of old cache values</li>
* <li>User deleted</li>
* </ul>
*
* @param event user event
*/
@Subscribe
public void onEvent(UserEvent event) {
if (event.getEventType().isPost()) {
if (isModificationEvent(event)) {
handleUserModificationEvent((UserModificationEvent) event);
} else {
handleUserEvent(event);
}
}
}
private boolean isModificationEvent(HandlerEvent<?> event) {
return event instanceof ModificationHandlerEvent;
}
private void handleUserEvent(UserEvent event) {
String username = event.getItem().getName();
logger.debug(
"fire authorization changed event for user {}, because of user {} event", username, event.getEventType()
);
fireEventForUser(username);
}
private void handleUserModificationEvent(UserModificationEvent event) {
String username = event.getItem().getId();
User beforeModification = event.getItemBeforeModification();
if (isAuthorizationDataModified(event.getItem(), beforeModification)) {
logger.debug(
"fire authorization changed event for user {}, because of a authorization relevant field has changed",
username
);
fireEventForUser(username);
} else {
logger.debug(
"authorization changed event for user {} is not fired, because no authorization relevant field has changed",
username
);
}
}
private boolean isAuthorizationDataModified(User user, User beforeModification) {
return user.isAdmin() != beforeModification.isAdmin() || user.isActive() != beforeModification.isActive();
}
private void fireEventForUser(String username) {
sendEvent(AuthorizationChangedEvent.createForUser(username));
}
/**
* Invalidates the whole cache, if a repository has changed. The cache get cleared for one of the following reasons:
* <ul>
* <li>New repository created</li>
* <li>Repository was removed</li>
* <li>Archived, Public readable or permission field of the repository was modified</li>
* </ul>
*
* @param event repository event
*/
@Subscribe
public void onEvent(RepositoryEvent event) {
if (event.getEventType().isPost()) {
if (isModificationEvent(event)) {
handleRepositoryModificationEvent((RepositoryModificationEvent) event);
} else {
handleRepositoryEvent(event);
}
}
}
private void handleRepositoryModificationEvent(RepositoryModificationEvent event) {
Repository repository = event.getItem();
if (isAuthorizationDataModified(repository, event.getItemBeforeModification())) {
logger.debug(
"fire authorization changed event, because a relevant field of repository {} has changed", repository.getName()
);
fireEventForEveryUser();
} else {
logger.debug(
"authorization changed event is not fired, because non relevant field of repository {} has changed",
repository.getName()
);
}
}
private boolean isAuthorizationDataModified(Repository repository, Repository beforeModification) {
return repository.isArchived() != beforeModification.isArchived()
|| repository.isPublicReadable() != beforeModification.isPublicReadable()
|| ! repository.getPermissions().equals(beforeModification.getPermissions());
}
private void fireEventForEveryUser() {
sendEvent(AuthorizationChangedEvent.createForEveryUser());
}
private void handleRepositoryEvent(RepositoryEvent event){
logger.debug(
"fire authorization changed event, because of received {} event for repository {}",
event.getEventType(), event.getItem().getName()
);
fireEventForEveryUser();
}
/**
* Invalidates the whole cache if a group permission has changed and invalidates the cached entries of a user, if a
* user permission has changed.
*
* @param event permission event
*/
@Subscribe
public void onEvent(StoredAssignedPermissionEvent event) {
if (event.getEventType().isPost()) {
StoredAssignedPermission permission = event.getPermission();
if (permission.isGroupPermission()) {
handleGroupPermissionChange(permission);
} else {
handleUserPermissionChange(permission);
}
}
}
private void handleGroupPermissionChange(StoredAssignedPermission permission) {
logger.debug(
"fire authorization changed event, because global group permission {} has changed",
permission.getId()
);
fireEventForEveryUser();
}
private void handleUserPermissionChange(StoredAssignedPermission permission) {
logger.debug(
"fire authorization changed event for user {}, because permission {} has changed",
permission.getName(), permission.getId()
);
fireEventForUser(permission.getName());
}
/**
* Invalidates the whole cache, if a group has changed. The cache get cleared for one of the following reasons:
* <ul>
* <li>New group created</li>
* <li>Group was removed</li>
* <li>Group members was modified</li>
* </ul>
*
* @param event group event
*/
@Subscribe
public void onEvent(GroupEvent event) {
if (event.getEventType().isPost()) {
if (isModificationEvent(event)) {
handleGroupModificationEvent((GroupModificationEvent) event);
} else {
handleGroupEvent(event);
}
}
}
private void handleGroupModificationEvent(GroupModificationEvent event) {
Group group = event.getItem();
if (isAuthorizationDataModified(group, event.getItemBeforeModification())) {
logger.debug("fire authorization changed event, because group {} has changed", group.getId());
fireEventForEveryUser();
} else {
logger.debug(
"authorization changed event is not fired, because non relevant field of group {} has changed",
group.getId()
);
}
}
private boolean isAuthorizationDataModified(Group group, Group beforeModification) {
return !group.getMembers().equals(beforeModification.getMembers());
}
private void handleGroupEvent(GroupEvent event){
logger.debug(
"fire authorization changed event, because of received group event {} for group {}",
event.getEventType(),
event.getItem().getId()
);
fireEventForEveryUser();
}
@VisibleForTesting
protected void sendEvent(AuthorizationChangedEvent event) {
ScmEventBus.getInstance().post(event);
}
}

View File

@@ -55,24 +55,17 @@ import org.slf4j.LoggerFactory;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.group.GroupEvent;
import sonia.scm.group.GroupNames;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryEvent;
import sonia.scm.user.User;
import sonia.scm.user.UserEvent;
import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.util.List;
import java.util.Set;
import sonia.scm.group.Group;
import sonia.scm.group.GroupModificationEvent;
import sonia.scm.repository.RepositoryModificationEvent;
import sonia.scm.user.UserModificationEvent;
/**
*
@@ -99,7 +92,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
LoggerFactory.getLogger(DefaultAuthorizationCollector.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
@@ -144,187 +137,14 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
return authorizationInfo;
}
/**
* Invalidates the cache of a user which was modified. The cache entries for the user will be invalidated for the
* following reasons:
* <ul>
* <li>Admin or Active flag was modified.</li>
* <li>New user created, for the case of old cache values</li>
* <li>User deleted</li>
* </ul>
*
* @param event user event
*/
@Subscribe
public void onEvent(UserEvent event)
{
if (event.getEventType().isPost())
{
User user = event.getItem();
String username = user.getId();
if (event instanceof UserModificationEvent)
{
User beforeModification = ((UserModificationEvent) event).getItemBeforeModification();
if (shouldCacheBeCleared(user, beforeModification))
{
logger.debug("invalidate cache of user {}, because of a permission relevant field has changed", username);
invalidateUserCache(username);
}
else
{
logger.debug("cache of user {} is not invalidated, because no permission relevant field has changed", username);
}
}
else
{
logger.debug("invalidate cache of user {}, because of user {} event", username, event.getEventType());
invalidateUserCache(username);
}
}
}
private boolean shouldCacheBeCleared(User user, User beforeModification)
{
return user.isAdmin() != beforeModification.isAdmin() || user.isActive() != beforeModification.isActive();
}
private void invalidateUserCache(final String username)
{
cache.removeAll((CacheKey item) -> username.equalsIgnoreCase(item.username));
}
/**
* Invalidates the whole cache, if a repository has changed. The cache get cleared for one of the following reasons:
* <ul>
* <li>New repository created</li>
* <li>Repository was removed</li>
* <li>Archived, Public readable or permission field of the repository was modified</li>
* </ul>
*
* @param event repository event
*/
@Subscribe
public void onEvent(RepositoryEvent event)
{
if (event.getEventType().isPost())
{
Repository repository = event.getItem();
if (event instanceof RepositoryModificationEvent)
{
Repository beforeModification = ((RepositoryModificationEvent) event).getItemBeforeModification();
if (shouldCacheBeCleared(repository, beforeModification))
{
logger.debug("clear cache, because a relevant field of repository {} has changed", repository.getName());
cache.clear();
}
else
{
logger.debug(
"cache is not invalidated, because non relevant field of repository {} has changed",
repository.getName()
);
}
}
else
{
logger.debug("clear cache, received {} event of repository {}", event.getEventType(), repository.getName());
cache.clear();
}
}
}
private boolean shouldCacheBeCleared(Repository repository, Repository beforeModification)
{
return repository.isArchived() != beforeModification.isArchived()
|| repository.isPublicReadable() != beforeModification.isPublicReadable()
|| ! repository.getPermissions().equals(beforeModification.getPermissions());
}
/**
* Invalidates the whole cache if a group permission has changed and invalidates the cached entries of a user, if a
* user permission has changed.
*
*
* @param event permission event
*/
@Subscribe
public void onEvent(StoredAssignedPermissionEvent event)
{
if (event.getEventType().isPost())
{
StoredAssignedPermission permission = event.getPermission();
if (permission.isGroupPermission())
{
logger.debug("clear cache, because global group permission {} has changed", permission.getId());
cache.clear();
}
else
{
logger.debug(
"clear cache of user {}, because permission {} has changed",
permission.getName(), event.getPermission().getId()
);
invalidateUserCache(permission.getName());
}
}
}
/**
* Invalidates the whole cache, if a group has changed. The cache get cleared for one of the following reasons:
* <ul>
* <li>New group created</li>
* <li>Group was removed</li>
* <li>Group members was modified</li>
* </ul>
*
* @param event group event
*/
@Subscribe
public void onEvent(GroupEvent event)
{
if (event.getEventType().isPost())
{
Group group = event.getItem();
if (event instanceof GroupModificationEvent)
{
Group beforeModification = ((GroupModificationEvent) event).getItemBeforeModification();
if (shouldCacheBeCleared(group, beforeModification))
{
logger.debug("clear cache, because group {} has changed", group.getId());
cache.clear();
}
else
{
logger.debug(
"cache is not invalidated, because non relevant field of group {} has changed",
group.getId()
);
}
}
else
{
logger.debug("clear cache, received group event {} for group {}", event.getEventType(), group.getId());
cache.clear();
}
}
}
private boolean shouldCacheBeCleared(Group group, Group beforeModification)
{
return !group.getMembers().equals(beforeModification.getMembers());
}
/**
* Method description
*
*
*
* @param principals
*
* @return
*/
AuthorizationInfo collect(PrincipalCollection principals)
public AuthorizationInfo collect(PrincipalCollection principals)
{
Preconditions.checkNotNull(principals, "principals parameter is required");
@@ -456,6 +276,25 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|| ((!perm.isGroupPermission()) && user.getName().equals(perm.getName()));
//J+
}
@Subscribe
public void invalidateCache(AuthorizationChangedEvent event) {
if (event.isEveryUserAffected()) {
invalidateUserCache(event.getNameOfAffectedUser());
} else {
invalidateCache();
}
}
private void invalidateUserCache(final String username) {
logger.info("invalidate cache for user {}, because of a received authorization event", username);
cache.removeAll((CacheKey item) -> username.equalsIgnoreCase(item.username));
}
private void invalidateCache() {
logger.info("invalidate cache, because of a received authorization event");
cache.clear();
}
//~--- inner classes --------------------------------------------------------

View File

@@ -78,6 +78,14 @@ Ext.apply(Ext.form.VTypes, {
return this.name(val);
},
usernameText: 'The username is invalid.'
usernameText: 'The username is invalid.',
emailRegex: /^[A-z0-9][\w.-]*@[A-z0-9][\w\-\.]*\.[A-z0-9][A-z0-9-]+$/,
// override extjs email format validation to match backend validation rules
// see https://bitbucket.org/sdorra/scm-manager/issues/909/new-gtld-support
email: function(email) {
return this.emailRegex.test(email);
}
});

View File

@@ -158,8 +158,7 @@ public abstract class CacheManagerTestBase<C extends Cache>
* @param c1
* @param c2
*/
protected void assertIsSame(Cache<String, String> c1,
Cache<String, String> c2)
protected void assertIsSame(Cache<String, String> c1, Cache<String, String> c2)
{
assertSame(c1, c2);
}

View File

@@ -76,8 +76,6 @@ public class CreateRepositoriesITCase extends AbstractAdminITCaseBase
*/
public CreateRepositoriesITCase(String repositoryType)
{
System.out.append("==> CreateRepositoriesITCase - ").println(
repositoryType);
this.repositoryType = repositoryType;
}
@@ -92,14 +90,14 @@ public class CreateRepositoriesITCase extends AbstractAdminITCaseBase
@Parameters
public static Collection<String[]> createParameters()
{
Collection<String[]> params = new ArrayList<String[]>();
Collection<String[]> params = new ArrayList<>();
params.add(new String[] { "git" });
params.add(new String[] { "git" });
params.add(new String[] { "svn" });
if (IOUtil.search("hg") != null)
{
params.add(new String[] { "git" });
params.add(new String[] { "hg" });
}
return params;

View File

@@ -125,8 +125,7 @@ public class DeactivatedUserITCase
public void testFailedAuthentication()
{
Client client = createClient();
ClientResponse response = authenticate(client, slarti.getName(),
"slart123");
ClientResponse response = authenticate(client, slarti.getName(), "slart123");
assertNotNull(response);
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus());
}

View File

@@ -0,0 +1,343 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.it;
import com.google.common.base.Charsets;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.UniformInterfaceException;
import java.io.IOException;
import java.util.UUID;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.xc.JaxbAnnotationIntrospector;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Test;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import static org.junit.Assert.*;
import org.junit.rules.ExpectedException;
import static sonia.scm.it.IntegrationTestUtil.*;
import static sonia.scm.it.RepositoryITUtil.*;
import sonia.scm.repository.Permission;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.user.User;
import sonia.scm.user.UserTestData;
/**
* Integration tests for git lfs.
*
* @author Sebastian Sdorra
*/
public class GitLfsITCase {
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private final ObjectMapper mapper = new ObjectMapper();
private Client adminClient;
private Repository repository;
public GitLfsITCase() {
mapper.setAnnotationIntrospector(new JaxbAnnotationIntrospector());
}
// lifecycle methods
@Before
public void setUpTestDependencies() {
adminClient = createAdminClient();
repository = createRepository(adminClient, RepositoryTestData.createHeartOfGold("git"));
}
@After
public void tearDownTestDependencies() {
deleteRepository(adminClient, repository.getId());
adminClient.destroy();
}
// tests
@Test
public void testLfsAPIWithAdminPermissions() throws IOException {
uploadAndDownload(adminClient);
}
@Test
public void testLfsAPIWithOwnerPermissions() throws IOException {
uploadAndDownloadAsUser(PermissionType.OWNER);
}
private void uploadAndDownloadAsUser(PermissionType permissionType) throws IOException {
User trillian = UserTestData.createTrillian();
trillian.setPassword("secret123");
createUser(trillian);
try {
repository.getPermissions().add(new Permission(trillian.getId(), permissionType));
modifyRepository(repository);
Client client = createClient();
authenticate(client, trillian.getId(), "secret123");
uploadAndDownload(client);
} finally {
removeUser(trillian);
}
}
@Test
public void testLfsAPIWithWritePermissions() throws IOException {
uploadAndDownloadAsUser(PermissionType.WRITE);
}
private void createUser(User user) {
adminClient.resource(REST_BASE_URL + "users.json").post(user);
}
private void modifyRepository(Repository repository) {
adminClient.resource(REST_BASE_URL + "repositories/" + repository.getId() + ".json").put(repository);
}
private void removeUser(User user) {
adminClient.resource(REST_BASE_URL + "users/" + user.getId() + ".json").delete();
}
@Test
public void testLfsAPIWithoutWritePermissions() throws IOException {
User trillian = UserTestData.createTrillian();
trillian.setPassword("secret123");
createUser(trillian);
expectedException.expect(UniformInterfaceException.class);
expectedException.expectMessage(Matchers.containsString("403"));
try {
repository.getPermissions().add(new Permission(trillian.getId(), PermissionType.READ));
modifyRepository(repository);
Client client = createClient();
authenticate(client, trillian.getId(), "secret123");
uploadAndDownload(client);
} finally {
removeUser(trillian);
}
}
@Test
public void testLfsDownloadWithReadPermissions() throws IOException {
User trillian = UserTestData.createTrillian();
trillian.setPassword("secret123");
createUser(trillian);
try {
repository.getPermissions().add(new Permission(trillian.getId(), PermissionType.READ));
modifyRepository(repository);
// upload data as admin
String data = UUID.randomUUID().toString();
byte[] dataAsBytes = data.getBytes(Charsets.UTF_8);
LfsObject lfsObject = upload(adminClient, dataAsBytes);
Client client = createClient();
authenticate(client, trillian.getId(), "secret123");
// download as user
byte[] downloadedData = download(client, lfsObject);
// assert both are equal
assertArrayEquals(dataAsBytes, downloadedData);
} finally {
removeUser(trillian);
}
}
// lfs api
private void uploadAndDownload(Client client) throws IOException {
String data = UUID.randomUUID().toString();
byte[] dataAsBytes = data.getBytes(Charsets.UTF_8);
LfsObject lfsObject = upload(client, dataAsBytes);
byte[] downloadedData = download(client, lfsObject);
assertArrayEquals(dataAsBytes, downloadedData);
}
private LfsObject upload(Client client, byte[] data) throws IOException {
LfsObject lfsObject = createLfsObject(data);
LfsRequestBody request = LfsRequestBody.createUploadRequest(lfsObject);
LfsResponseBody response = request(client, request);
String uploadURL = response.objects[0].actions.upload.href;
client.resource(uploadURL).put(data);
return lfsObject;
}
private LfsResponseBody request(Client client, LfsRequestBody request) throws IOException {
String batchUrl = createBatchUrl();
String requestAsString = mapper.writeValueAsString(request);
return client
.resource(batchUrl)
.accept("application/vnd.git-lfs+json")
.header("Content-Type", "application/vnd.git-lfs+json")
.post(LfsResponseBody.class, requestAsString);
}
private String createBatchUrl() {
String url = repository.createUrl(BASE_URL);
return url + "/info/lfs/objects/batch";
}
private byte[] download(Client client, LfsObject lfsObject) throws IOException {
LfsRequestBody request = LfsRequestBody.createDownloadRequest(lfsObject);
LfsResponseBody response = request(client, request);
String downloadUrl = response.objects[0].actions.download.href;
return client.resource(downloadUrl).get(byte[].class);
}
private LfsObject createLfsObject(byte[] data) {
Sha256Hash hash = new Sha256Hash(data);
String oid = hash.toHex();
return new LfsObject(oid, data.length);
}
// LFS DTO objects
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
private static class LfsRequestBody {
private String operation;
private String[] transfers = new String[]{ "basic" };
private LfsObject[] objects;
public LfsRequestBody() {
}
private LfsRequestBody(String operation, LfsObject[] objects) {
this.operation = operation;
this.objects = objects;
}
public static LfsRequestBody createUploadRequest(LfsObject object) {
return new LfsRequestBody("upload", new LfsObject[]{object});
}
public static LfsRequestBody createDownloadRequest(LfsObject object) {
return new LfsRequestBody("download", new LfsObject[]{object});
}
}
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
private static class LfsResponseBody {
private LfsObject[] objects;
public LfsResponseBody() {
}
public LfsResponseBody(LfsObject[] objects) {
this.objects = objects;
}
}
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
private static class LfsObject {
private String oid;
private long size;
private LfsActions actions;
public LfsObject() {
}
public LfsObject(String oid, long size) {
this.oid = oid;
this.size = size;
}
public LfsObject(String oid, long size, LfsActions actions) {
this.oid = oid;
this.size = size;
this.actions = actions;
}
}
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
private static class LfsActions {
private LfsAction upload;
private LfsAction download;
public LfsActions() {
}
}
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
private static class LfsAction {
private String href;
public LfsAction() {
}
public LfsAction(String href) {
this.href = href;
}
}
}

View File

@@ -0,0 +1,143 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.it;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
import com.sun.jersey.api.client.Client;
import java.io.File;
import java.io.IOException;
import org.junit.After;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import static sonia.scm.it.IntegrationTestUtil.*;
import static sonia.scm.it.RepositoryITUtil.*;
import sonia.scm.repository.Person;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.client.api.ClientCommand;
import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.repository.client.api.RepositoryClientFactory;
/**
* Integration test for RepositoryPathMatching with ".git" and without ".git".
*
* @author Sebastian Sdorra
* @since 1.54
*/
public class GitRepositoryPathMatcherITCase {
private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory();
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
private Client apiClient;
private Repository repository;
@Before
public void setUp() {
apiClient = createAdminClient();
Repository testRepository = RepositoryTestData.createHeartOfGold("git");
this.repository = createRepository(apiClient, testRepository);
}
@After
public void tearDown() {
deleteRepository(apiClient, repository.getId());
}
// tests begin
@Test
public void testWithoutDotGit() throws IOException {
String urlWithoutDotGit = createUrl();
cloneAndPush(urlWithoutDotGit);
}
@Test
public void testWithDotGit() throws IOException {
String urlWithDotGit = createUrl() + ".git";
cloneAndPush(urlWithDotGit);
}
// tests end
private String createUrl() {
return BASE_URL + "git/" + repository.getName();
}
private void cloneAndPush( String url ) throws IOException {
cloneRepositoryAndPushFiles(url);
cloneRepositoryAndCheckFiles(url);
}
private void cloneRepositoryAndPushFiles(String url) throws IOException {
RepositoryClient repositoryClient = createRepositoryClient(url);
Files.write("a", new File(repositoryClient.getWorkingCopy(), "a.txt"), Charsets.UTF_8);
repositoryClient.getAddCommand().add("a.txt");
commit(repositoryClient, "added a");
Files.write("b", new File(repositoryClient.getWorkingCopy(), "b.txt"), Charsets.UTF_8);
repositoryClient.getAddCommand().add("b.txt");
commit(repositoryClient, "added b");
}
private void cloneRepositoryAndCheckFiles(String url) throws IOException {
RepositoryClient repositoryClient = createRepositoryClient(url);
File workingCopy = repositoryClient.getWorkingCopy();
File a = new File(workingCopy, "a.txt");
assertTrue(a.exists());
assertEquals("a", Files.toString(a, Charsets.UTF_8));
File b = new File(workingCopy, "b.txt");
assertTrue(b.exists());
assertEquals("b", Files.toString(b, Charsets.UTF_8));
}
private void commit(RepositoryClient repositoryClient, String message) throws IOException {
repositoryClient.getCommitCommand().commit(
new Person("scmadmin", "scmadmin@scm-manager.org"), message
);
if ( repositoryClient.isCommandSupported(ClientCommand.PUSH) ) {
repositoryClient.getPushCommand().push();
}
}
private RepositoryClient createRepositoryClient(String url) throws IOException {
return REPOSITORY_CLIENT_FACTORY.create("git", url, ADMIN_USERNAME, ADMIN_PASSWORD, tempFolder.newFolder());
}
}

View File

@@ -181,7 +181,7 @@ public class RepositoryHookITCase extends AbstractAdminITCaseBase
private Changeset commit(String message) throws IOException {
Changeset a = repositoryClient.getCommitCommand().commit(
new Person("scmadmin", "scmadmin@scm-manager.org"), "added a"
new Person("scmadmin", "scmadmin@scm-manager.org"), message
);
if ( repositoryClient.isCommandSupported(ClientCommand.PUSH) ) {
repositoryClient.getPushCommand().push();

View File

@@ -0,0 +1,225 @@
/**
* Copyright (c) 2014, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.repository;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Provider;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.ThreadContext;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import static org.mockito.Mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import sonia.scm.SCMContextProvider;
import sonia.scm.Type;
import sonia.scm.cache.GuavaCacheManager;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AuthorizationCollector;
import sonia.scm.security.DefaultKeyGenerator;
import sonia.scm.security.KeyGenerator;
import sonia.scm.security.SecuritySystem;
import sonia.scm.user.UserTestData;
/**
* Performance test for {@link RepositoryManager#getAll()}.
*
* @see <a href="https://goo.gl/PD1AeM">Issue 781</a>
* @author Sebastian Sdorra
* @since 1.52
*/
@RunWith(MockitoJUnitRunner.class)
public class DefaultRepositoryManagerPerfTest {
private static final int REPOSITORY_COUNT = 2000;
private static final String REPOSITORY_TYPE = "perf";
@Mock
private SCMContextProvider contextProvider;
@Mock
private RepositoryDAO repositoryDAO;
private final ScmConfiguration configuration = new ScmConfiguration();
private final KeyGenerator keyGenerator = new DefaultKeyGenerator();
@Mock
private RepositoryHandler repositoryHandler;
private DefaultRepositoryManager repositoryManager;
@Mock
private AuthorizationCollector authzCollector;
/**
* Setup object under test.
*/
@Before
public void setUpObjectUnderTest(){
when(repositoryHandler.getType()).thenReturn(new Type(REPOSITORY_TYPE, REPOSITORY_TYPE));
Set<RepositoryHandler> handlerSet = ImmutableSet.of(repositoryHandler);
RepositoryMatcher repositoryMatcher = new RepositoryMatcher(Collections.<RepositoryPathMatcher>emptySet());
repositoryManager = new DefaultRepositoryManager(
configuration,
contextProvider,
keyGenerator,
repositoryDAO,
handlerSet,
repositoryMatcher
);
setUpTestRepositories();
GuavaCacheManager cacheManager = new GuavaCacheManager();
DefaultSecurityManager securityManager = new DefaultSecurityManager(new DummyRealm(authzCollector, cacheManager));
ThreadContext.bind(securityManager);
}
/**
* Tear down test objects.
*/
@After
public void tearDown(){
ThreadContext.unbindSecurityManager();
}
/**
* Start performance test and ensure that the timeout is not reached.
*/
@Test(timeout = 6000l)
public void perfTestGetAll(){
SecurityUtils.getSubject().login(new UsernamePasswordToken("trillian", "secret"));
List<Long> times = new ArrayList<>();
for ( int i=0; i<3; i++ ) {
times.add(benchGetAll());
}
long average = calculateAverage(times);
double value = (double) average / TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS);
// Too bad this functionality is not exposed as a regular method call
System.out.println( String.format("%.4g s", value) );
}
private long calculateAverage(List<Long> times) {
Long sum = 0l;
if(!times.isEmpty()) {
for (Long time : times) {
sum += time;
}
return Math.round(sum.doubleValue() / times.size());
}
return sum;
}
private long benchGetAll(){
Stopwatch sw = Stopwatch.createStarted();
System.out.append("found ").append(String.valueOf(repositoryManager.getAll().size()));
sw.stop();
System.out.append(" in ").println(sw);
return sw.elapsed(TimeUnit.MILLISECONDS);
}
private void setUpTestRepositories() {
Map<String,Repository> repositories = new LinkedHashMap<>();
for ( int i=0; i<REPOSITORY_COUNT; i++ ) {
Repository repository = createTestRepository(i);
repositories.put(repository.getId(), repository);
}
when(repositoryDAO.getAll()).thenReturn(repositories.values());
}
private Repository createTestRepository(int number){
Repository repository = new Repository(keyGenerator.createKey(), REPOSITORY_TYPE, "repo-" + number);
repository.getPermissions().add(new Permission("trillian", PermissionType.READ));
return repository;
}
static class DummyRealm extends AuthorizingRealm {
private final AuthorizationCollector authzCollector;
public DummyRealm(AuthorizationCollector authzCollector, org.apache.shiro.cache.CacheManager cacheManager) {
this.authzCollector = authzCollector;
setCredentialsMatcher(new AllowAllCredentialsMatcher());
setCacheManager(cacheManager);
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
SimplePrincipalCollection spc = new SimplePrincipalCollection(token.getPrincipal(), REPOSITORY_TYPE);
spc.add(UserTestData.createTrillian(), REPOSITORY_TYPE);
return new SimpleAuthenticationInfo(spc, REPOSITORY_TYPE);
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return authzCollector.collect();
}
}
private static class SetProvider implements Provider {
@Override
public Object get() {
return Collections.emptySet();
}
}
}

View File

@@ -55,12 +55,12 @@ import static org.hamcrest.Matchers.*;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import org.apache.shiro.authz.UnauthorizedException;
import org.hamcrest.Matchers;
import org.junit.Rule;
import org.junit.rules.ExpectedException;
import org.mockito.invocation.InvocationOnMock;
@@ -492,19 +492,18 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository, Re
@Test
public void getRepositoryFromRequestUriTest() throws RepositoryException, IOException {
RepositoryManager m = createManager();
m.init(contextProvider);
createRepository(m, new Repository("1", "hg", "scm"));
createRepository(m, new Repository("2", "hg", "scm-test"));
createRepository(m, new Repository("3", "git", "project1/test-1"));
createRepository(m, new Repository("4", "git", "project1/test-2"));
assertEquals("scm", m.getFromUri("hg/scm").getName());
assertEquals("scm-test", m.getFromUri("hg/scm-test").getName());
assertEquals("scm-test", m.getFromUri("/hg/scm-test").getName());
assertEquals("project1/test-1",
m.getFromUri("/git/project1/test-1").getName());
assertEquals("project1/test-1",
m.getFromUri("/git/project1/test-1/ka/some/path").getName());
assertEquals("project1/test-1", m.getFromUri("/git/project1/test-1").getName());
assertEquals("project1/test-1", m.getFromUri("/git/project1/test-1/ka/some/path").getName());
assertNull(m.getFromUri("/git/project1/test-3/ka/some/path"));
}
@@ -543,7 +542,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository, Re
configuration.setEnableRepositoryArchive(archiveEnabled);
return new DefaultRepositoryManager(configuration, contextProvider,
keyGenerator, repositoryDAO, handlerSet);
keyGenerator, repositoryDAO, handlerSet, createRepositoryMatcher());
}
private void createRepository(RepositoryManager m, Repository repository)
@@ -569,6 +568,10 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository, Re
assertEquals(repo.getCreationDate(), other.getCreationDate());
assertEquals(repo.getLastModified(), other.getLastModified());
}
private RepositoryMatcher createRepositoryMatcher() {
return new RepositoryMatcher(Collections.<RepositoryPathMatcher>emptySet());
}
private Repository createRepository(Repository repository) throws RepositoryException, IOException {
manager.create(repository);

View File

@@ -0,0 +1,88 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.repository;
import com.google.common.collect.Sets;
import java.util.Set;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.Before;
/**
* Unit tests for {@link RepositoryMatcher}.
*
* @author Sebastian Sdorra
* @since 1.54
*/
public class RepositoryMatcherTest {
private RepositoryMatcher matcher;
@Before
public void setUp() {
Set<RepositoryPathMatcher> pathMatchers = Sets.<RepositoryPathMatcher>newHashSet(new AbcRepositoryPathMatcher());
this.matcher = new RepositoryMatcher(pathMatchers);
}
@Test
public void testMatches() {
assertFalse(matcher.matches(repository("hg", "scm"), "hg", "scm-test/ka"));
assertFalse(matcher.matches(repository("git", "scm-test"), "hg", "scm-test"));
assertTrue(matcher.matches(repository("hg", "scm-test"), "hg", "scm-test/ka"));
assertTrue(matcher.matches(repository("hg", "scm-test"), "hg", "scm-test"));
}
@Test
public void testMatchesWithCustomPathMatcher() {
assertFalse(matcher.matches(repository("abc", "scm"), "hg", "/long/path/with/abc"));
assertTrue(matcher.matches(repository("abc", "scm"), "abc", "/long/path/with/abc"));
}
private Repository repository(String type, String name) {
return new Repository(type + "-" + name, type, name);
}
private static class AbcRepositoryPathMatcher implements RepositoryPathMatcher {
@Override
public boolean isPathMatching(Repository repository, String path) {
return path.endsWith("abc");
}
@Override
public String getType() {
return "abc";
}
}
}

View File

@@ -0,0 +1,256 @@
/**
* Copyright (c) 2014, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.security;
import com.google.common.collect.Lists;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.Before;
import sonia.scm.HandlerEventType;
import sonia.scm.group.Group;
import sonia.scm.group.GroupEvent;
import sonia.scm.group.GroupModificationEvent;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryEvent;
import sonia.scm.repository.RepositoryModificationEvent;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.user.User;
import sonia.scm.user.UserEvent;
import sonia.scm.user.UserModificationEvent;
import sonia.scm.user.UserTestData;
/**
* Unit tests for {@link AuthorizationChangedEventProducer}.
*
* @author Sebastian Sdorra
*/
public class AuthorizationChangedEventProducerTest {
private StoringAuthorizationChangedEventProducer producer;
@Before
public void setUpProducer() {
producer = new StoringAuthorizationChangedEventProducer();
}
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.user.UserEvent)}.
*/
@Test
public void testOnUserEvent()
{
User user = UserTestData.createDent();
producer.onEvent(new UserEvent(HandlerEventType.BEFORE_CREATE, user));
assertEventIsNotFired();
producer.onEvent(new UserEvent(HandlerEventType.CREATE, user));
assertUserEventIsFired("dent");
}
private void assertEventIsNotFired(){
assertNull(producer.event);
}
private void assertUserEventIsFired(String username){
assertNotNull(producer.event);
assertTrue(producer.event.isEveryUserAffected());
assertEquals(username, producer.event.getNameOfAffectedUser());
}
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.user.UserEvent)} with modified user.
*/
@Test
public void testOnUserModificationEvent()
{
User user = UserTestData.createDent();
User userModified = UserTestData.createDent();
userModified.setDisplayName("Super Dent");
producer.onEvent(new UserModificationEvent(HandlerEventType.BEFORE_CREATE, userModified, user));
assertEventIsNotFired();
producer.onEvent(new UserModificationEvent(HandlerEventType.CREATE, userModified, user));
assertEventIsNotFired();
userModified.setAdmin(true);
producer.onEvent(new UserModificationEvent(HandlerEventType.BEFORE_CREATE, userModified, user));
assertEventIsNotFired();
producer.onEvent(new UserModificationEvent(HandlerEventType.CREATE, userModified, user));
assertUserEventIsFired("dent");
}
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.group.GroupEvent)}.
*/
@Test
public void testOnGroupEvent()
{
Group group = new Group("xml", "base");
producer.onEvent(new GroupEvent(HandlerEventType.BEFORE_CREATE, group));
assertEventIsNotFired();
producer.onEvent(new GroupEvent(HandlerEventType.CREATE, group));
assertGlobalEventIsFired();
}
private void assertGlobalEventIsFired(){
assertNotNull(producer.event);
assertFalse(producer.event.isEveryUserAffected());
}
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.group.GroupEvent)} with modified groups.
*/
@Test
public void testOnGroupModificationEvent()
{
Group group = new Group("xml", "base");
Group modifiedGroup = new Group("xml", "base");
producer.onEvent(new GroupModificationEvent(HandlerEventType.BEFORE_MODIFY, modifiedGroup, group));
assertEventIsNotFired();
producer.onEvent(new GroupModificationEvent(HandlerEventType.MODIFY, modifiedGroup, group));
assertEventIsNotFired();
modifiedGroup.add("test");
producer.onEvent(new GroupModificationEvent(HandlerEventType.MODIFY, modifiedGroup, group));
assertGlobalEventIsFired();
}
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.repository.RepositoryEvent)}.
*/
@Test
public void testOnRepositoryEvent()
{
Repository repository = RepositoryTestData.createHeartOfGold();
producer.onEvent(new RepositoryEvent(HandlerEventType.BEFORE_CREATE, repository));
assertEventIsNotFired();
producer.onEvent(new RepositoryEvent(HandlerEventType.CREATE, repository));
assertGlobalEventIsFired();
}
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.repository.RepositoryEvent)} with modified
* repository.
*/
@Test
public void testOnRepositoryModificationEvent()
{
Repository repositoryModified = RepositoryTestData.createHeartOfGold();
repositoryModified.setName("test123");
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
Repository repository = RepositoryTestData.createHeartOfGold();
repository.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.BEFORE_CREATE, repositoryModified, repository));
assertEventIsNotFired();
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertEventIsNotFired();
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertEventIsNotFired();
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test123")));
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertGlobalEventIsFired();
resetStoredEvent();
repositoryModified.setPermissions(
Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.READ, true))
);
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertGlobalEventIsFired();
resetStoredEvent();
repositoryModified.setPermissions(
Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.WRITE))
);
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertGlobalEventIsFired();
}
private void resetStoredEvent(){
producer.event = null;
}
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.security.StoredAssignedPermissionEvent)}.
*/
@Test
public void testOnStoredAssignedPermissionEvent()
{
StoredAssignedPermission groupPermission = new StoredAssignedPermission(
"123", new AssignedPermission("_authenticated", true, "repository:read:*")
);
producer.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, groupPermission));
assertEventIsNotFired();
producer.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.CREATE, groupPermission));
assertGlobalEventIsFired();
resetStoredEvent();
StoredAssignedPermission userPermission = new StoredAssignedPermission(
"123", new AssignedPermission("trillian", false, "repository:read:*")
);
producer.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, userPermission));
assertEventIsNotFired();
resetStoredEvent();
producer.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.CREATE, userPermission));
assertUserEventIsFired("trillian");
}
private static class StoringAuthorizationChangedEventProducer extends AuthorizationChangedEventProducer {
private AuthorizationChangedEvent event;
@Override
protected void sendEvent(AuthorizationChangedEvent event) {
this.event = event;
}
}
}

View File

@@ -50,22 +50,14 @@ import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.Rule;
import org.mockito.runners.MockitoJUnitRunner;
import sonia.scm.HandlerEventType;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.group.Group;
import sonia.scm.group.GroupEvent;
import sonia.scm.group.GroupModificationEvent;
import sonia.scm.group.GroupNames;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryEvent;
import sonia.scm.repository.RepositoryModificationEvent;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.user.User;
import sonia.scm.user.UserEvent;
import sonia.scm.user.UserModificationEvent;
import sonia.scm.user.UserTestData;
/**
@@ -82,7 +74,7 @@ public class DefaultAuthorizationCollectorTest {
@Mock
private CacheManager cacheManager;
@Mock
private RepositoryDAO repositoryDAO;
@@ -104,160 +96,6 @@ public class DefaultAuthorizationCollectorTest {
collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem);
}
/**
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.user.UserEvent)}.
*/
@Test
public void testOnUserEvent()
{
User user = UserTestData.createDent();
collector.onEvent(new UserEvent(HandlerEventType.BEFORE_CREATE, user));
verify(cache, never()).clear();
collector.onEvent(new UserEvent(HandlerEventType.CREATE, user));
verify(cache).removeAll(Mockito.any(Predicate.class));
}
/**
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.user.UserEvent)} with modified user.
*/
@Test
public void testOnUserModificationEvent()
{
User user = UserTestData.createDent();
User userModified = UserTestData.createDent();
userModified.setDisplayName("Super Dent");
collector.onEvent(new UserModificationEvent(HandlerEventType.BEFORE_CREATE, userModified, user));
verify(cache, never()).removeAll(Mockito.any(Predicate.class));
collector.onEvent(new UserModificationEvent(HandlerEventType.CREATE, userModified, user));
verify(cache, never()).removeAll(Mockito.any(Predicate.class));
userModified.setAdmin(true);
collector.onEvent(new UserModificationEvent(HandlerEventType.BEFORE_CREATE, userModified, user));
verify(cache, never()).removeAll(Mockito.any(Predicate.class));
collector.onEvent(new UserModificationEvent(HandlerEventType.CREATE, userModified, user));
verify(cache).removeAll(Mockito.any(Predicate.class));
}
/**
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.group.GroupEvent)}.
*/
@Test
public void testOnGroupEvent()
{
Group group = new Group("xml", "base");
collector.onEvent(new GroupEvent(HandlerEventType.BEFORE_CREATE, group));
verify(cache, never()).clear();
collector.onEvent(new GroupEvent(HandlerEventType.CREATE, group));
verify(cache).clear();
}
/**
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.group.GroupEvent)} with modified groups.
*/
@Test
public void testOnGroupModificationEvent()
{
Group group = new Group("xml", "base");
Group modifiedGroup = new Group("xml", "base");
collector.onEvent(new GroupModificationEvent(HandlerEventType.BEFORE_MODIFY, modifiedGroup, group));
verify(cache, never()).clear();
collector.onEvent(new GroupModificationEvent(HandlerEventType.MODIFY, modifiedGroup, group));
verify(cache, never()).clear();
modifiedGroup.add("test");
collector.onEvent(new GroupModificationEvent(HandlerEventType.MODIFY, modifiedGroup, group));
verify(cache).clear();
}
/**
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.repository.RepositoryEvent)}.
*/
@Test
public void testOnRepositoryEvent()
{
Repository repository = RepositoryTestData.createHeartOfGold();
collector.onEvent(new RepositoryEvent(HandlerEventType.BEFORE_CREATE, repository));
verify(cache, never()).clear();
collector.onEvent(new RepositoryEvent(HandlerEventType.CREATE, repository));
verify(cache).clear();
}
/**
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.repository.RepositoryEvent)} with modified repository.
*/
@Test
public void testOnRepositoryModificationEvent()
{
Repository repositoryModified = RepositoryTestData.createHeartOfGold();
repositoryModified.setName("test123");
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
Repository repository = RepositoryTestData.createHeartOfGold();
repository.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
collector.onEvent(new RepositoryModificationEvent(HandlerEventType.BEFORE_CREATE, repositoryModified, repository));
verify(cache, never()).clear();
collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
verify(cache, never()).clear();
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
verify(cache, never()).clear();
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test123")));
collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
verify(cache).clear();
repositoryModified.setPermissions(
Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.READ, true))
);
collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
verify(cache, times(2)).clear();
repositoryModified.setPermissions(
Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.WRITE))
);
collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
verify(cache, times(3)).clear();
}
/**
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.security.StoredAssignedPermissionEvent)}.
*/
@Test
public void testOnStoredAssignedPermissionEvent()
{
StoredAssignedPermission groupPermission = new StoredAssignedPermission(
"123", new AssignedPermission("_authenticated", true, "repository:read:*")
);
collector.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, groupPermission));
verify(cache, never()).clear();
collector.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.CREATE, groupPermission));
verify(cache).clear();
StoredAssignedPermission userPermission = new StoredAssignedPermission(
"123", new AssignedPermission("trillian", false, "repository:read:*")
);
collector.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, userPermission));
verify(cache, never()).removeAll(Mockito.any(Predicate.class));
verify(cache).clear();
collector.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.CREATE, userPermission));
verify(cache).removeAll(Mockito.any(Predicate.class));
verify(cache).clear();
}
/**
* Tests {@link AuthorizationCollector#collect()} without user role.
*/
@@ -386,8 +224,7 @@ public class DefaultAuthorizationCollectorTest {
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two"));
}
private void authenticate(User user, String group, String... groups)
{
private void authenticate(User user, String group, String... groups) {
SimplePrincipalCollection spc = new SimplePrincipalCollection();
spc.add(user.getName(), "unit");
spc.add(user, "unit");
@@ -396,4 +233,16 @@ public class DefaultAuthorizationCollectorTest {
shiro.setSubject(subject);
}
}
/**
* Tests {@link AuthorizationCollector#invalidateCache(sonia.scm.security.AuthorizationChangedEvent)}.
*/
@Test
public void testInvalidateCache() {
collector.invalidateCache(AuthorizationChangedEvent.createForEveryUser());
verify(cache).clear();
collector.invalidateCache(AuthorizationChangedEvent.createForUser("dent"));
verify(cache).removeAll(Mockito.any(Predicate.class));
}
}