diff --git a/.hgignore b/.hgignore index a49329d7a5..6ff185f670 100644 --- a/.hgignore +++ b/.hgignore @@ -28,3 +28,4 @@ Desktop DF$ \.idea$ # jrebel rebel.xml +\.pyc diff --git a/pom.xml b/pom.xml index 29de08fb05..7f377a7f18 100644 --- a/pom.xml +++ b/pom.xml @@ -259,7 +259,7 @@ true true - http://download.oracle.com/javase/6/docs/api/ + http://download.oracle.com/javase/8/docs/api/ http://download.oracle.com/docs/cd/E17802_01/products/products/servlet/2.5/docs/servlet-2_5-mr2/ http://jersey.java.net/nonav/apidocs/${jersey.version}/jersey/ https://google.github.io/guice/api-docs/${guice.version}/javadoc @@ -483,7 +483,7 @@ 1.1.10 3.0.1 4.0 - 1.19.3 + 1.19.4 1.2.0 @@ -497,8 +497,8 @@ 1.4.0-RC2 - v4.5.0.201609210915-r-scm1 - 1.8.14-scm1 + v4.5.2.201704071617-r-scm1 + 1.8.15-scm1 16.0.1 diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBody.java b/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBody.java index dd5ec6f425..fdc779a9f9 100644 --- a/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBody.java +++ b/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBody.java @@ -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 diff --git a/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpResponse.java b/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpResponse.java index 1cb087710b..66893cbd68 100644 --- a/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpResponse.java +++ b/scm-core/src/main/java/sonia/scm/net/ahc/AdvancedHttpResponse.java @@ -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 object type * @param type object type diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryPathMatcher.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryPathMatcher.java new file mode 100644 index 0000000000..74d8c87f5f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPathMatcher.java @@ -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(); +} diff --git a/scm-core/src/main/java/sonia/scm/security/AuthorizationChangedEvent.java b/scm-core/src/main/java/sonia/scm/security/AuthorizationChangedEvent.java new file mode 100644 index 0000000000..0ef929e1fa --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/AuthorizationChangedEvent.java @@ -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); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/store/Blob.java b/scm-core/src/main/java/sonia/scm/store/Blob.java index 195697472a..a569e61ab5 100644 --- a/scm-core/src/main/java/sonia/scm/store/Blob.java +++ b/scm-core/src/main/java/sonia/scm/store/Blob.java @@ -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(); + + } diff --git a/scm-core/src/test/java/sonia/scm/i18n/I18nMessagesTest.java b/scm-core/src/test/java/sonia/scm/i18n/I18nMessagesTest.java index 0760c4efb6..1e317ff62e 100644 --- a/scm-core/src/test/java/sonia/scm/i18n/I18nMessagesTest.java +++ b/scm-core/src/test/java/sonia/scm/i18n/I18nMessagesTest.java @@ -58,6 +58,17 @@ public class I18nMessagesTest @Test public void testI18n() { + /* + lookup-order for this test: + - TM_en (es specified, but not ava) + - TM_ + - 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); diff --git a/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java b/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java index 620c2fae9d..3eaddf2d36 100644 --- a/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java +++ b/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java @@ -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")); diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlob.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlob.java index 5352d0c4c9..679aaef814 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlob.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlob.java @@ -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; + } + } } diff --git a/scm-plugin-backend/pom.xml b/scm-plugin-backend/pom.xml new file mode 100644 index 0000000000..1d3621b8ea --- /dev/null +++ b/scm-plugin-backend/pom.xml @@ -0,0 +1,165 @@ + + + + 4.0.0 + + + scm + sonia.scm + 1.55-SNAPSHOT + + + sonia.scm + scm-plugin-backend + war + 1.55-SNAPSHOT + ${project.artifactId} + + + + + javax.servlet + servlet-api + ${servlet.version} + provided + + + + + + javax.transaction + jta + 1.1 + provided + + + + + + org.slf4j + jcl-over-slf4j + ${slf4j.version} + + + + org.slf4j + log4j-over-slf4j + ${slf4j.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + + + + org.freemarker + freemarker + ${freemarker.version} + + + + sonia.scm + scm-core + 1.55-SNAPSHOT + + + + com.sun.jersey.contribs + jersey-guice + ${jersey.version} + + + + rome + rome + 1.0 + + + + net.sf.ehcache + ehcache-core + ${ehcache.version} + + + + org.imgscalr + imgscalr-lib + 4.2 + + + + + + org.apache.shiro + shiro-web + ${shiro.version} + + + + org.apache.shiro + shiro-guice + ${shiro.version} + + + + org.apache.shiro + shiro-ehcache + ${shiro.version} + + + + + + src/main/webapp/template/** + + + + + + + + com.mycila.maven-license-plugin + maven-license-plugin + 1.9.0 + +
http://download.scm-manager.org/licenses/mvn-license.txt
+ + src/** + **/test/** + + + target/** + .hg/** + **/html5.js + **/*.html + **/fancybox/** + + true +
+
+ + + org.mortbay.jetty + jetty-maven-plugin + ${jetty.version} + + 8004 + STOP + + /scm-plugin-backend + + ${project.build.javaLevel} + ${project.build.javaLevel} + ${project.build.sourceEncoding} + 0 + + + +
+ + scm-plugin-backend +
+ +
diff --git a/scm-plugin-backend/src/main/java/sonia/scm/plugin/AdminAccountConfiguration.java b/scm-plugin-backend/src/main/java/sonia/scm/plugin/AdminAccountConfiguration.java new file mode 100644 index 0000000000..46bbc06c64 --- /dev/null +++ b/scm-plugin-backend/src/main/java/sonia/scm/plugin/AdminAccountConfiguration.java @@ -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; +} diff --git a/scm-plugin-backend/src/main/java/sonia/scm/plugin/security/SecurityModule.java b/scm-plugin-backend/src/main/java/sonia/scm/plugin/security/SecurityModule.java new file mode 100644 index 0000000000..9be130375e --- /dev/null +++ b/scm-plugin-backend/src/main/java/sonia/scm/plugin/security/SecurityModule.java @@ -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); + } +} diff --git a/scm-plugins/scm-git-plugin/pom.xml b/scm-plugins/scm-git-plugin/pom.xml index 4fff4ddd27..7013c237cb 100644 --- a/scm-plugins/scm-git-plugin/pom.xml +++ b/scm-plugins/scm-git-plugin/pom.xml @@ -38,6 +38,12 @@ ${jgit.version} + + sonia.jgit + org.eclipse.jgit.lfs.server + ${jgit.version} + + commons-lang commons-lang @@ -80,11 +86,6 @@ - - jgit-repository - http://download.eclipse.org/jgit/maven - - maven.scm-manager.org scm-manager release repository diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java index 4678cab65d..2338cc3b46 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java @@ -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 */ diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryPathMatcher.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryPathMatcher.java new file mode 100644 index 0000000000..a6a4023eec --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryPathMatcher.java @@ -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 <>.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; + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index aa881bf884..1538269ee1 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -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; } /** diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java index a25dc05ddd..1f07753f1e 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java @@ -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()); + } + } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java index 5ddd000490..2ddf4b3de9 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java @@ -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 +public class GitRepositoryResolver implements RepositoryResolver { /** 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 --------------------------------------------------------------- diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java index 2507db150a..3d3442ce2a 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java @@ -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); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitUserAgentProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitUserAgentProvider.java index 0fc6495066..fc7d3d6445 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitUserAgentProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitUserAgentProvider.java @@ -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 * @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); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java index 686e4dfbd7..e8e62b52cc 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java @@ -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. + *
    + *
  • Batch API:
  • + *
      + *
    • used to provide the client with information on how handle the large files of a repository.
    • + *
    • response contains the information where to perform the actual upload and download of the large objects.
    • + *
    + *
  • Transfer API:
  • + *
      + *
    • receives and provides the actual large objects (resolves the pointer placed in the file of the working copy).
    • + *
    • invoked only after the Batch API has been questioned about what to do with the large files
    • + *
    + *
  • Regular Git Http API:
  • + *
      + *
    • regular git http wire protocol, use by normal git clients.
    • + *
    + *
  • Browser Overview:
  • + *
      + *
    • short repository overview for browser clients.
    • + *
    + *
  • + *
+ */ + 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, + *

+ * - 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, + *

+ * - 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; + } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java new file mode 100644 index 0000000000..eebd6b8f2b --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java @@ -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. + *

+ * 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. + *

+ * 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); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsStoreRemoveListener.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsStoreRemoveListener.java new file mode 100644 index 0000000000..3d69dcabe6 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsStoreRemoveListener.java @@ -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); + } + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/ScmBlobLfsRepository.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/ScmBlobLfsRepository.java new file mode 100644 index 0000000000..46a58f6f07 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/ScmBlobLfsRepository.java @@ -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; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java new file mode 100644 index 0000000000..2ca10559a2 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java @@ -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()); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/ScmFileTransferServlet.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/ScmFileTransferServlet.java new file mode 100644 index 0000000000..324140b432 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/ScmFileTransferServlet.java @@ -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. + *

+ * This implementation is based on {@link FileLfsServlet} but adjusted to work with + * servlet-2.5 instead of servlet-3.1. + *

+ * + * @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. + *

+ * Actual implementation is based on org.eclipse.jgit.lfs.server.fs.ObjectDownloadListener 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. + *

+ * Actual implementation is based on org.eclipse.jgit.lfs.server.fs.ObjectUploadListener 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; + } + } + +} + + diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/ScmLfsProtocolServlet.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/ScmLfsProtocolServlet.java new file mode 100644 index 0000000000..332cf12e09 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/ScmLfsProtocolServlet.java @@ -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; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryPathMatcherTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryPathMatcherTest.java new file mode 100644 index 0000000000..7adc4a6913 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryPathMatcherTest.java @@ -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); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitUtilTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitUtilTest.java index 10813098bc..1f81f401d1 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitUtilTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitUtilTest.java @@ -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; + } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitRepositoryClientProvider.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitRepositoryClientProvider.java index 2226ad04df..f545540a38 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitRepositoryClientProvider.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitRepositoryClientProvider.java @@ -191,7 +191,7 @@ public class GitRepositoryClientProvider extends RepositoryClientProvider @Override public File getWorkingCopy() { - return git.getRepository().getDirectory(); + return git.getRepository().getWorkTree(); } //~--- fields --------------------------------------------------------------- diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java new file mode 100644 index 0000000000..df4827a9fc --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java @@ -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(); + } + } + +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitRepositoryResolverTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitRepositoryResolverTest.java new file mode 100644 index 0000000000..d41e0acafc --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitRepositoryResolverTest.java @@ -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()); + } + } + +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitUserAgentProviderTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitUserAgentProviderTest.java index b16580c4a5..7b9fe944b5 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitUserAgentProviderTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitUserAgentProviderTest.java @@ -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 */ -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); + } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/ScmGitServletTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/ScmGitServletTest.java new file mode 100644 index 0000000000..4013980cb1 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/ScmGitServletTest.java @@ -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)); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java new file mode 100644 index 0000000000..2eb8968405 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java @@ -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); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsStoreRemoveListenerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsStoreRemoveListenerTest.java new file mode 100644 index 0000000000..5a1bfdbf9a --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsStoreRemoveListenerTest.java @@ -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 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)); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java new file mode 100644 index 0000000000..c670032089 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java @@ -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; + } + + +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/ScmFileTransferServletTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/ScmFileTransferServletTest.java new file mode 100644 index 0000000000..3caa02b272 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/ScmFileTransferServletTest.java @@ -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())); + + } +} diff --git a/scm-plugins/scm-hg-plugin/pom.xml b/scm-plugins/scm-hg-plugin/pom.xml index 05980904fe..67b6b3e684 100644 --- a/scm-plugins/scm-hg-plugin/pom.xml +++ b/scm-plugins/scm-hg-plugin/pom.xml @@ -22,7 +22,7 @@ com.aragost.javahg javahg - 0.7-scm1 + 0.8-scm1 com.google.guava diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index 907ae5f411..518f229011 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -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'), - ]) -} \ No newline at end of file diff --git a/scm-server/pom.xml b/scm-server/pom.xml index 687658bff5..64172d6673 100644 --- a/scm-server/pom.xml +++ b/scm-server/pom.xml @@ -172,7 +172,7 @@ com.github.sdorra nativepkg-maven-plugin - 1.1.3 + 1.1.4 @@ -204,6 +204,7 @@ ${project.basedir}/src/main/nativepkg/create-user + ${project.basedir}/src/main/nativepkg/clear-cache diff --git a/scm-server/src/main/nativepkg/clear-cache b/scm-server/src/main/nativepkg/clear-cache new file mode 100644 index 0000000000..01d7140357 --- /dev/null +++ b/scm-server/src/main/nativepkg/clear-cache @@ -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 diff --git a/scm-test/pom.xml b/scm-test/pom.xml index 55cd2bfcb1..94948a830a 100644 --- a/scm-test/pom.xml +++ b/scm-test/pom.xml @@ -63,12 +63,6 @@ tmatesoft release repository https://maven.tmatesoft.com/content/repositories/releases - - - jgit-repository - jgit release repository - http://download.eclipse.org/jgit/maven - diff --git a/scm-test/src/main/java/sonia/scm/AbstractTestBase.java b/scm-test/src/main/java/sonia/scm/AbstractTestBase.java index 47ee59b075..13cde0391e 100644 --- a/scm-test/src/main/java/sonia/scm/AbstractTestBase.java +++ b/scm-test/src/main/java/sonia/scm/AbstractTestBase.java @@ -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())); + } } } diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index a43548368f..c8b6572e92 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -218,20 +218,9 @@ - org.codehaus.enunciate - enunciate-jersey-rt + com.webcohesion.enunciate + enunciate-core-annotations ${enunciate.version} - - - - jackson-jaxrs - org.codehaus.jackson - - - jackson-xc - org.codehaus.jackson - - @@ -545,10 +534,14 @@ target/scm-it default 2.53.1 - 1.31 + 2.9.1 1.0 0.8.17 Tomcat + e1 + javascript:S3827 + **.js + src/main/webapp/resources/extjs/**,src/main/webapp/resources/moment/**,src/main/webapp/resources/syntaxhighlighter/** @@ -793,8 +786,35 @@ - org.codehaus.enunciate - maven-enunciate-plugin + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + + copy-enunciate-configuration + compile + + copy-resources + + + ${project.build.directory} + + + src/main/doc + true + + **/enunciate.xml + + + + + + + + + + com.webcohesion.enunciate + enunciate-maven-plugin ${enunciate.version} @@ -805,14 +825,21 @@ - src/main/doc/enunciate.xml - ${project.build.directory}/restdocs + ${project.build.directory}/enunciate.xml + ${project.build.directory} + restdocs - org.codehaus.enunciate - enunciate-jersey - ${enunciate.version} + com.webcohesion.enunciate + enunciate-top + 2.9.1 + + + com.webcohesion.enunciate + enunciate-swagger + + diff --git a/scm-webapp/src/main/doc/enunciate.xml b/scm-webapp/src/main/doc/enunciate.xml index 504566a55d..9bed97455a 100644 --- a/scm-webapp/src/main/doc/enunciate.xml +++ b/scm-webapp/src/main/doc/enunciate.xml @@ -39,30 +39,33 @@ Description: Enunciate configuration --> - + + SCM-Manager API + + + SCM-Manager API +

This page describes the RESTful Web Service API of SCM-Manager ${project.version}.

+ ]]> + + - + - - - - - + + + - - - - - - + diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java index 8a577b6993..f1bbdf2ce1 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java @@ -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); diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractPermissionResource.java index 29384ba020..e9e63d16d8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractPermissionResource.java @@ -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.
- *
- * Status codes: - *
    - *
  • 201 add successful
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- * Status codes: - *
    - *
  • 200 delete successful
  • - *
  • 400 bad request, permission id does not belong to the user or group
  • - *
  • 404 not found, no permission with the specified id available
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- * Status codes: - *
    - *
  • 204 update successful
  • - *
  • 400 bad request, permission id does not belong to the user or group
  • - *
  • 404 not found, no permission with the specified id available
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 400 bad request, permission id does not belong to the user or group
  • - *
  • 404 not found, no permission with the specified id available
  • - *
  • 500 internal server error
  • - *
- * + * 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.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 500 internal server error
  • - *
+ * 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 getAll() { diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AuthenticationResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AuthenticationResource.java index 9a717deb20..2c5615c1a0 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AuthenticationResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AuthenticationResource.java @@ -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.
- *
- *
    - *
  • 200 success
  • - *
  • 400 bad request, required parameter is missing.
  • - *
  • 401 unauthorized, the specified username or password is wrong
  • - *
  • 500 internal server error
  • - *
- * + * 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.
- *
- *
    - *
  • 200 success
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- *
    - *
  • 200 success
  • - *
  • 401 unauthorized, user is not authenticated and public access is disabled.
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- *
    - *
  • 200 success
  • - *
  • 401 unauthorized, user is not authenticated and public access is disabled.
  • - *
  • 500 internal server error
  • - *
+ * 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; diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java index 90743126bc..a9e904c89f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java @@ -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.
- *
- * Status codes: - *
    - *
  • 200 success
  • - *
  • 400 bad request, the old password is not correct
  • - *
  • 500 internal server error
  • - *
+ * 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) diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/CipherResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/CipherResource.java index 27de46ce03..bbfdb363c0 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/CipherResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/CipherResource.java @@ -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.
- *
- *
    - *
  • 200 success
  • - *
  • 500 internal server error
  • - *
+ * 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) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ConfigurationResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ConfigurationResource.java index 6c888470ad..2fb6bdda3d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ConfigurationResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ConfigurationResource.java @@ -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 { diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupResource.java index ce5a26a6a1..808aaa498b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupResource.java @@ -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 { @@ -102,15 +103,7 @@ public class GroupResource //~--- methods -------------------------------------------------------------- /** - * Creates a new group.
- * This method requires admin privileges.
- *
- * Status codes: - *
    - *
  • 201 create success
  • - *
  • 403 forbidden, the current user has no admin privileges
  • - *
  • 500 internal server error
  • - *
+ * Creates a new group. Note: 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.
- * This method requires admin privileges.
- *
- * Status codes: - *
    - *
  • 201 delete success
  • - *
  • 403 forbidden, the current user has no admin privileges
  • - *
  • 500 internal server error
  • - *
+ * Deletes a group. Note: 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.
- * This method requires admin privileges.
- *
- * Status codes: - *
    - *
  • 201 update successful
  • - *
  • 403 forbidden, the current user has no admin privileges
  • - *
  • 500 internal server error
  • - *
+ * Modifies the given group. Note: 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.
- * This method requires admin privileges.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 403 forbidden, the current user has no admin privileges
  • - *
  • 404 not found, no group with the specified id/name available
  • - *
  • 500 internal server error
  • - *
+ * Fetches a group by its name or id. Note: 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.
- * This method requires admin privileges.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 403 forbidden, the current user has no admin privileges
  • - *
  • 500 internal server error
  • - *
+ * Returns all groups. Note: 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> createGenericEntity( Collection 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() { diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/KeyResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/KeyResource.java index 3a6e10a51f..6a4c56a643 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/KeyResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/KeyResource.java @@ -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.
- *
- *
    - *
  • 200 success
  • - *
  • 500 internal server error
  • - *
+ * Generates a unique key. Note: 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() { diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/PluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/PluginResource.java index 5c9ea468d3..c605c7c29a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/PluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/PluginResource.java @@ -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.
- *
- *
    - *
  • 200 success
  • - *
  • 412 precondition failed
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- *
    - *
  • 200 success
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- *
    - *
  • 200 success
  • - *
  • 412 precondition failed
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- *
    - *
  • 200 success
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- *
    - *
  • 200 success
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- *
    - *
  • 200 success
  • - *
  • 500 internal server error
  • - *
+ * 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 getAll() { @@ -261,17 +264,16 @@ public class PluginResource } /** - * Returns all available plugins.
- *
- *
    - *
  • 200 success
  • - *
  • 500 internal server error
  • - *
+ * 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 getAvailable() { @@ -279,17 +281,16 @@ public class PluginResource } /** - * Returns all plugins which are available for update.
- *
- *
    - *
  • 200 success
  • - *
  • 500 internal server error
  • - *
+ * 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 getAvailableUpdates() { @@ -297,17 +298,16 @@ public class PluginResource } /** - * Returns all installed plugins.
- *
- *
    - *
  • 200 success
  • - *
  • 500 internal server error
  • - *
+ * 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 getInstalled() { @@ -315,17 +315,16 @@ public class PluginResource } /** - * Returns all plugins for the overview.
- *
- *
    - *
  • 200 success
  • - *
  • 500 internal server error
  • - *
+ * 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 getOverview() { diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java index 74e8310e05..728c935fc1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java @@ -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.
- * - * Status codes: - *
    - *
  • 201 created
  • - *
  • 400 bad request, the import bundle feature is not supported by this - * type of repositories or the parameters are not valid.
  • - *
  • 500 internal server error
  • - *
  • 409 conflict, a repository with the name already exists.
  • - *
+ * bundle file is passed to the {@link UnbundleCommandBuilder}. Note: 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.
- * - * Status codes: - *
    - *
  • 201 created
  • - *
  • 400 bad request, the import bundle feature is not supported by this - * type of repositories or the parameters are not valid.
  • - *
  • 500 internal server error
  • - *
  • 409 conflict, a repository with the name already exists.
  • - *
- * + * workaround of the javascript ui extjs. Note: 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.
- * - * Status codes: - *
    - *
  • 201 created
  • - *
  • 400 bad request, the import by url feature is not supported by this - * type of repositories or the parameters are not valid.
  • - *
  • 409 conflict, a repository with the name already exists.
  • - *
  • 500 internal server error
  • - *
+ * repository. Note: 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.
- *
- * Status codes: - *
    - *
  • 200 ok, successful
  • - *
  • 400 bad request, the import feature is not - * supported by this type of repositories.
  • - *
  • 500 internal server error
  • - *
+ * directory. Note: 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.
- *
- * Status codes: - *
    - *
  • 200 ok, successful
  • - *
  • 400 bad request, the import feature is not - * supported by this type of repositories.
  • - *
  • 500 internal server error
  • - *
+ * directories. Note: 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.
- *
- * Status codes: - *
    - *
  • 200 ok, successful
  • - *
  • 400 bad request, the import feature is not - * supported by this type of repositories.
  • - *
  • 500 internal server error
  • - *
+ * of failed directories. Note: 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.
- *
- * Status codes: - *
    - *
  • 200 ok, successful
  • - *
  • 400 bad request, the import feature is not - * supported by this type of repositories.
  • - *
  • 500 internal server error
  • - *
+ * feature. Note: 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() { diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java index 640721bc64..ad092911ee 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java @@ -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 +public class RepositoryResource extends AbstractManagerResource { /** Field description */ @@ -147,22 +147,22 @@ public class RepositoryResource //~--- methods -------------------------------------------------------------- /** - * Creates a new repository.
- * This method requires admin privileges.
- *
- * Status codes: - *
    - *
  • 201 create success
  • - *
  • 403 forbidden, the current user has no admin privileges
  • - *
  • 500 internal server error
  • - *
+ * Creates a new repository.Note: 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.
- * This method requires owner privileges.
- *
- * Status codes: - *
    - *
  • 201 delete success
  • - *
  • 403 forbidden, the current user has no owner privileges
  • - *
  • - * 412 forbidden, the repository is not archived, - * this error occurs only with enabled repository archive. - *
  • - *
  • 500 internal server error
  • - *
+ * Deletes a repository. Note: 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.
- * Status codes: - *
    - *
  • 201 re run success
  • - *
  • 403 forbidden, the current user has no owner privileges
  • - *
  • 404 could not find repository
  • - *
  • 500 internal server error
  • - *
+ * 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.
- * This method requires owner privileges.
- *
- * Status codes: - *
    - *
  • 201 update successful
  • - *
  • 403 forbidden, the current user has no owner privileges
  • - *
  • 500 internal server error
  • - *
+ * Modifies the given repository. Note: 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.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 404 not found, no repository with the specified id available
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 400 bad request, the blame feature is not - * supported by this type of repositories.
  • - *
  • 404 not found, if the repository or the path could not be found
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 400 bad request, the content feature is not - * supported by this type of repositories.
  • - *
  • 404 not found, if the repository or the path could not be found
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 400 bad request, the browse feature is not - * supported by this type of repositories.
  • - *
  • 404 not found, if the repository or the path could not be found
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 404 not found, - * no repository with the specified type and name available
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 400 bad request, the changeset feature is not - * supported by this type of repositories.
  • - *
  • 404 not found, if the repository or - * the revision could not be found
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 400 bad request, the changeset feature is not - * supported by this type of repositories.
  • - *
  • 404 not found, if the repository or the path could not be found
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 400 bad request, the content feature is not - * supported by this type of repositories.
  • - *
  • 404 not found, if the repository or the path could not be found
  • - *
  • 500 internal server error
  • - *
+ * 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}.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 400 bad request, the content feature is not - * supported by this type of repositories.
  • - *
  • 404 not found, if the repository or the path could not be found
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 400 bad request, the content feature is not - * supported by this type of repositories.
  • - *
  • 404 not found, if the repository or the path could not be found
  • - *
  • 500 internal server error
  • - *
+ * 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 { diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java index 1633feb270..056ad7c7db 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java @@ -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 { diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SearchResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SearchResource.java index afadc513c2..d951fcc2ca 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SearchResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SearchResource.java @@ -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.
- *
- *
    - *
  • 200 success
  • - *
  • 500 internal server error
  • - *
+ * 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.
- *
- *
    - *
  • 200 success
  • - *
  • 500 internal server error
  • - *
+ * 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) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SecuritySystemResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SecuritySystemResource.java index b4baee98e6..f9e95232db 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SecuritySystemResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SecuritySystemResource.java @@ -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; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SupportResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SupportResource.java index 752491f86b..ae6d0d2e8e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SupportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SupportResource.java @@ -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 { diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserResource.java index 42bfe702c7..3516eab883 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserResource.java @@ -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 { @@ -107,15 +108,7 @@ public class UserResource extends AbstractManagerResource //~--- methods -------------------------------------------------------------- /** - * Creates a new user.
- * This method requires admin privileges.
- *
- * Status codes: - *
    - *
  • 201 create success
  • - *
  • 403 forbidden, the current user has no admin privileges
  • - *
  • 500 internal server error
  • - *
+ * Creates a new user. Note: 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 * @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 } /** - * Deletes a user.
- * This method requires admin privileges.
- *
- * Status codes: - *
    - *
  • 201 delete success
  • - *
  • 403 forbidden, the current user has no admin privileges
  • - *
  • 500 internal server error
  • - *
+ * Deletes a user. Note: This method requires admin privileges. * * @param name the name of the user to delete. * @@ -147,6 +140,12 @@ public class UserResource extends AbstractManagerResource */ @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 } /** - * Modifies the given user.
- * This method requires admin privileges.
- *
- * Status codes: - *
    - *
  • 201 update successful
  • - *
  • 403 forbidden, the current user has no admin privileges
  • - *
  • 500 internal server error
  • - *
+ * Modifies the given user. Note: 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 */ @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 //~--- get methods ---------------------------------------------------------- /** - * Returns a user.
- * This method requires admin privileges.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 403 forbidden, the current user has no admin privileges
  • - *
  • 404 not found, no user with the specified id/name available
  • - *
  • 500 internal server error
  • - *
+ * Returns a user. Note: 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 @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 } /** - * Returns all users.
- * This method requires admin privileges.
- *
- * Status codes: - *
    - *
  • 200 get successful
  • - *
  • 403 forbidden, the current user has no admin privileges
  • - *
  • 500 internal server error
  • - *
+ * Returns all users. Note: 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 */ @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 //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @param items - * - * @return - */ @Override protected GenericEntity> createGenericEntity( Collection items) @@ -270,24 +253,12 @@ public class UserResource extends AbstractManagerResource ; } - /** - * 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 } } - /** - * Method description - * - * - * @param users - * - * @return - */ @Override protected Collection prepareForReturn(Collection users) { @@ -326,14 +289,6 @@ public class UserResource extends AbstractManagerResource return users; } - /** - * Method description - * - * - * @param user - * - * @return - */ @Override protected User prepareForReturn(User user) { @@ -342,42 +297,18 @@ public class UserResource extends AbstractManagerResource 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(); diff --git a/scm-webapp/src/main/java/sonia/scm/cache/GuavaCache.java b/scm-webapp/src/main/java/sonia/scm/cache/GuavaCache.java index 6380719322..8e97d3fd4c 100644 --- a/scm-webapp/src/main/java/sonia/scm/cache/GuavaCache.java +++ b/scm-webapp/src/main/java/sonia/scm/cache/GuavaCache.java @@ -63,8 +63,7 @@ public class GuavaCache /** * 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 @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 this.copyStrategy = CopyStrategy.NONE; } } - + //~--- methods -------------------------------------------------------------- /** diff --git a/scm-webapp/src/main/java/sonia/scm/cache/GuavaCacheManager.java b/scm-webapp/src/main/java/sonia/scm/cache/GuavaCacheManager.java index c46adb9fd3..43e86c762c 100644 --- a/scm-webapp/src/main/java/sonia/scm/cache/GuavaCacheManager.java +++ b/scm-webapp/src/main/java/sonia/scm/cache/GuavaCacheManager.java @@ -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 = diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index 901b27f78f..8ddb02d9ca 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -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 handlerSet) + RepositoryDAO repositoryDAO, Set 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 types; + + /** Field description */ + private RepositoryMatcher repositoryMatcher; } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/RepositoryMatcher.java b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryMatcher.java new file mode 100644 index 0000000000..d0ad30e84f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryMatcher.java @@ -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 pathMatchers; + + /** + * Creates a new instance. + * + * @param pathMatchers injected set of {@link RepositoryPathMatcher}. + */ + @Inject + public RepositoryMatcher(Set 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"; + } + + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java new file mode 100644 index 0000000000..c21ec17d18 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java @@ -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: + *
    + *
  • Admin or Active flag was modified.
  • + *
  • New user created, for the case of old cache values
  • + *
  • User deleted
  • + *
+ * + * @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: + *
    + *
  • New repository created
  • + *
  • Repository was removed
  • + *
  • Archived, Public readable or permission field of the repository was modified
  • + *
+ * + * @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: + *
    + *
  • New group created
  • + *
  • Group was removed
  • + *
  • Group members was modified
  • + *
+ * + * @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); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index c86b6c325e..d868b81499 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -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: - *
    - *
  • Admin or Active flag was modified.
  • - *
  • New user created, for the case of old cache values
  • - *
  • User deleted
  • - *
- * - * @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: - *
    - *
  • New repository created
  • - *
  • Repository was removed
  • - *
  • Archived, Public readable or permission field of the repository was modified
  • - *
- * - * @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: - *
    - *
  • New group created
  • - *
  • Group was removed
  • - *
  • Group members was modified
  • - *
- * - * @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 -------------------------------------------------------- diff --git a/scm-webapp/src/main/webapp/resources/js/override/ext.form.vtypes.js b/scm-webapp/src/main/webapp/resources/js/override/ext.form.vtypes.js index 91d5307577..eb751bbbb2 100644 --- a/scm-webapp/src/main/webapp/resources/js/override/ext.form.vtypes.js +++ b/scm-webapp/src/main/webapp/resources/js/override/ext.form.vtypes.js @@ -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); + } }); \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/cache/CacheManagerTestBase.java b/scm-webapp/src/test/java/sonia/scm/cache/CacheManagerTestBase.java index d2e1ec2213..716ab67e3b 100644 --- a/scm-webapp/src/test/java/sonia/scm/cache/CacheManagerTestBase.java +++ b/scm-webapp/src/test/java/sonia/scm/cache/CacheManagerTestBase.java @@ -158,8 +158,7 @@ public abstract class CacheManagerTestBase * @param c1 * @param c2 */ - protected void assertIsSame(Cache c1, - Cache c2) + protected void assertIsSame(Cache c1, Cache c2) { assertSame(c1, c2); } diff --git a/scm-webapp/src/test/java/sonia/scm/it/CreateRepositoriesITCase.java b/scm-webapp/src/test/java/sonia/scm/it/CreateRepositoriesITCase.java index 6ef841b2df..3d240c96f5 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/CreateRepositoriesITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/CreateRepositoriesITCase.java @@ -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 createParameters() { - Collection params = new ArrayList(); + Collection 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; diff --git a/scm-webapp/src/test/java/sonia/scm/it/DeactivatedUserITCase.java b/scm-webapp/src/test/java/sonia/scm/it/DeactivatedUserITCase.java index 425ed07df6..5d30115fbb 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/DeactivatedUserITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/DeactivatedUserITCase.java @@ -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()); } diff --git a/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java b/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java new file mode 100644 index 0000000000..f8adb90ff1 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java @@ -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; + } + + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/it/GitRepositoryPathMatcherITCase.java b/scm-webapp/src/test/java/sonia/scm/it/GitRepositoryPathMatcherITCase.java new file mode 100644 index 0000000000..079e2c520e --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/it/GitRepositoryPathMatcherITCase.java @@ -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()); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java b/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java index 5a58fd0a40..e7b85056e5 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java @@ -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(); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java new file mode 100644 index 0000000000..90a78e63c6 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java @@ -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 Issue 781 + * @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 handlerSet = ImmutableSet.of(repositoryHandler); + RepositoryMatcher repositoryMatcher = new RepositoryMatcher(Collections.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 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 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 repositories = new LinkedHashMap<>(); + for ( int i=0; iemptySet()); + } private Repository createRepository(Repository repository) throws RepositoryException, IOException { manager.create(repository); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/RepositoryMatcherTest.java b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryMatcherTest.java new file mode 100644 index 0000000000..c832bb8691 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryMatcherTest.java @@ -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 pathMatchers = Sets.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"; + } + + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java b/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java new file mode 100644 index 0000000000..b45e0d72e9 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java @@ -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; + } + + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index 3bdb2536ab..8dd6ed0d53 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -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); } -} \ No newline at end of file + /** + * 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)); + } + +}