mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-06 15:42:12 +01:00
merge with branch 1.x
This commit is contained in:
8
pom.xml
8
pom.xml
@@ -259,7 +259,7 @@
|
||||
<author>true</author>
|
||||
<keywords>true</keywords>
|
||||
<links>
|
||||
<link>http://download.oracle.com/javase/6/docs/api/</link>
|
||||
<link>http://download.oracle.com/javase/8/docs/api/</link>
|
||||
<link>http://download.oracle.com/docs/cd/E17802_01/products/products/servlet/2.5/docs/servlet-2_5-mr2/</link>
|
||||
<link>http://jersey.java.net/nonav/apidocs/${jersey.version}/jersey/</link>
|
||||
<link>https://google.github.io/guice/api-docs/${guice.version}/javadoc</link>
|
||||
@@ -483,7 +483,7 @@
|
||||
<logback.version>1.1.10</logback.version>
|
||||
<servlet.version>3.0.1</servlet.version>
|
||||
<guice.version>4.0</guice.version>
|
||||
<jersey.version>1.19.3</jersey.version>
|
||||
<jersey.version>1.19.4</jersey.version>
|
||||
|
||||
<!-- event bus -->
|
||||
<legman.version>1.2.0</legman.version>
|
||||
@@ -497,8 +497,8 @@
|
||||
<shiro.version>1.4.0-RC2</shiro.version>
|
||||
|
||||
<!-- repostitory libraries -->
|
||||
<jgit.version>v4.5.0.201609210915-r-scm1</jgit.version>
|
||||
<svnkit.version>1.8.14-scm1</svnkit.version>
|
||||
<jgit.version>v4.5.2.201704071617-r-scm1</jgit.version>
|
||||
<svnkit.version>1.8.15-scm1</svnkit.version>
|
||||
|
||||
<!-- util libraries -->
|
||||
<guava.version>16.0.1</guava.version>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -253,7 +253,9 @@ public abstract class AdvancedHttpResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the response content from xml to the given type.
|
||||
* Transforms the response content to the given type. The method will use
|
||||
* the {@link ContentTransformer} which is responsible for the the given
|
||||
* content type.
|
||||
*
|
||||
* @param <T> object type
|
||||
* @param type object type
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -58,6 +58,17 @@ public class I18nMessagesTest
|
||||
@Test
|
||||
public void testI18n()
|
||||
{
|
||||
/*
|
||||
lookup-order for this test:
|
||||
- TM_en (es specified, but not ava)
|
||||
- TM_<execution-locale>
|
||||
- TM
|
||||
|
||||
This means that, if there is no default locale specified, this test accidentally passes on non-german machines, an fails on german machines, since the execution locale is de_DE, which is checked even before the fallback locale is considered.
|
||||
*/
|
||||
|
||||
Locale.setDefault(Locale.ENGLISH);
|
||||
|
||||
TestMessages msg = I18nMessages.get(TestMessages.class);
|
||||
|
||||
assertEquals("Normal Key", msg.normalKey);
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -75,4 +75,13 @@ public final class FileBlob implements Blob {
|
||||
return new FileOutputStream(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
if (this.file.isFile()) {
|
||||
return this.file.length();
|
||||
} else {
|
||||
//to sum up all other cases, in which we cannot determine a size
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
165
scm-plugin-backend/pom.xml
Normal file
165
scm-plugin-backend/pom.xml
Normal file
@@ -0,0 +1,165 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<artifactId>scm</artifactId>
|
||||
<groupId>sonia.scm</groupId>
|
||||
<version>1.55-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<groupId>sonia.scm</groupId>
|
||||
<artifactId>scm-plugin-backend</artifactId>
|
||||
<packaging>war</packaging>
|
||||
<version>1.55-SNAPSHOT</version>
|
||||
<name>${project.artifactId}</name>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>servlet-api</artifactId>
|
||||
<version>${servlet.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- fix javadoc -->
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.transaction</groupId>
|
||||
<artifactId>jta</artifactId>
|
||||
<version>1.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- logging -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jcl-over-slf4j</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>log4j-over-slf4j</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>${logback.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.freemarker</groupId>
|
||||
<artifactId>freemarker</artifactId>
|
||||
<version>${freemarker.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>sonia.scm</groupId>
|
||||
<artifactId>scm-core</artifactId>
|
||||
<version>1.55-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.sun.jersey.contribs</groupId>
|
||||
<artifactId>jersey-guice</artifactId>
|
||||
<version>${jersey.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>rome</groupId>
|
||||
<artifactId>rome</artifactId>
|
||||
<version>1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.sf.ehcache</groupId>
|
||||
<artifactId>ehcache-core</artifactId>
|
||||
<version>${ehcache.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.imgscalr</groupId>
|
||||
<artifactId>imgscalr-lib</artifactId>
|
||||
<version>4.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- security -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-web</artifactId>
|
||||
<version>${shiro.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-guice</artifactId>
|
||||
<version>${shiro.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-ehcache</artifactId>
|
||||
<version>${shiro.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<sonar.exclusions>src/main/webapp/template/**</sonar.exclusions>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.mycila.maven-license-plugin</groupId>
|
||||
<artifactId>maven-license-plugin</artifactId>
|
||||
<version>1.9.0</version>
|
||||
<configuration>
|
||||
<header>http://download.scm-manager.org/licenses/mvn-license.txt</header>
|
||||
<includes>
|
||||
<include>src/**</include>
|
||||
<include>**/test/**</include>
|
||||
</includes>
|
||||
<excludes>
|
||||
<exclude>target/**</exclude>
|
||||
<exclude>.hg/**</exclude>
|
||||
<exclude>**/html5.js</exclude>
|
||||
<exclude>**/*.html</exclude>
|
||||
<exclude>**/fancybox/**</exclude>
|
||||
</excludes>
|
||||
<strictCheck>true</strictCheck>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.mortbay.jetty</groupId>
|
||||
<artifactId>jetty-maven-plugin</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
<configuration>
|
||||
<stopPort>8004</stopPort>
|
||||
<stopKey>STOP</stopKey>
|
||||
<webApp>
|
||||
<contextPath>/scm-plugin-backend</contextPath>
|
||||
</webApp>
|
||||
<source>${project.build.javaLevel}</source>
|
||||
<target>${project.build.javaLevel}</target>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
<scanIntervalSeconds>0</scanIntervalSeconds>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
|
||||
<finalName>scm-plugin-backend</finalName>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,12 @@
|
||||
<version>${jgit.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>sonia.jgit</groupId>
|
||||
<artifactId>org.eclipse.jgit.lfs.server</artifactId>
|
||||
<version>${jgit.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-lang</groupId>
|
||||
<artifactId>commons-lang</artifactId>
|
||||
@@ -80,11 +86,6 @@
|
||||
|
||||
<repositories>
|
||||
|
||||
<repository>
|
||||
<id>jgit-repository</id>
|
||||
<url>http://download.eclipse.org/jgit/maven</url>
|
||||
</repository>
|
||||
|
||||
<repository>
|
||||
<id>maven.scm-manager.org</id>
|
||||
<name>scm-manager release repository</name>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.repository;
|
||||
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
/**
|
||||
* Matches git repositories with ".git" and without ".git".
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.54
|
||||
*/
|
||||
@Extension
|
||||
public class GitRepositoryPathMatcher implements RepositoryPathMatcher {
|
||||
|
||||
@Override
|
||||
public boolean isPathMatching(Repository repository, String path) {
|
||||
String repositoryName = repository.getName();
|
||||
|
||||
if (path.startsWith(repositoryName)) {
|
||||
|
||||
String pathPart = path.substring(repositoryName.length());
|
||||
|
||||
// git repository may also be named <<repo-name>>.git by convention
|
||||
if (pathPart.startsWith(GitRepositoryHandler.DOT_GIT)) {
|
||||
// if this is the case, just also cut it away
|
||||
pathPart = pathPart.substring(GitRepositoryHandler.DOT_GIT.length());
|
||||
}
|
||||
|
||||
return Util.isEmpty(pathPart) || pathPart.startsWith(HttpUtil.SEPARATOR_PATH);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return GitRepositoryHandler.TYPE_NAME;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ package sonia.scm.web;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
||||
@@ -63,13 +64,11 @@ import javax.servlet.http.HttpServletRequest;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class GitRepositoryResolver
|
||||
implements RepositoryResolver<HttpServletRequest>
|
||||
public class GitRepositoryResolver implements RepositoryResolver<HttpServletRequest>
|
||||
{
|
||||
|
||||
/** the logger for GitRepositoryResolver */
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(GitRepositoryResolver.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(GitRepositoryResolver.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
@@ -114,20 +113,14 @@ public class GitRepositoryResolver
|
||||
|
||||
if (config.isValid())
|
||||
{
|
||||
File gitdir = new File(config.getRepositoryDirectory(), repositoryName);
|
||||
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("try to open git repository at {}", gitdir);
|
||||
}
|
||||
|
||||
if (!gitdir.exists())
|
||||
{
|
||||
File gitdir = findRepository(config.getRepositoryDirectory(), repositoryName);
|
||||
if (gitdir == null) {
|
||||
throw new RepositoryNotFoundException(repositoryName);
|
||||
}
|
||||
|
||||
logger.debug("try to open git repository at {}", gitdir);
|
||||
|
||||
repository = RepositoryCache.open(FileKey.lenient(gitdir, FS.DETECTED),
|
||||
true);
|
||||
repository = RepositoryCache.open(FileKey.lenient(gitdir, FS.DETECTED), true);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -139,17 +132,39 @@ public class GitRepositoryResolver
|
||||
throw new ServiceNotEnabledException();
|
||||
}
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
throw new RepositoryNotFoundException(repositoryName, e);
|
||||
}
|
||||
catch (IOException e)
|
||||
catch (RuntimeException | IOException e)
|
||||
{
|
||||
throw new RepositoryNotFoundException(repositoryName, e);
|
||||
}
|
||||
|
||||
return repository;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
File findRepository(File parentDirectory, String repositoryName) {
|
||||
File repositoryDirectory = new File(parentDirectory, repositoryName);
|
||||
if (repositoryDirectory.exists()) {
|
||||
return repositoryDirectory;
|
||||
}
|
||||
|
||||
if (endsWithDotGit(repositoryName)) {
|
||||
String repositoryNameWithoutDotGit = repositoryNameWithoutDotGit(repositoryName);
|
||||
repositoryDirectory = new File(parentDirectory, repositoryNameWithoutDotGit);
|
||||
if (repositoryDirectory.exists()) {
|
||||
return repositoryDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean endsWithDotGit(String repositoryName) {
|
||||
return repositoryName.endsWith(GitRepositoryHandler.DOT_GIT);
|
||||
}
|
||||
|
||||
private String repositoryNameWithoutDotGit(String repositoryName) {
|
||||
return repositoryName.substring(0, repositoryName.length() - GitRepositoryHandler.DOT_GIT.length());
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -35,63 +35,89 @@ package sonia.scm.web;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.base.Strings;
|
||||
import java.util.Locale;
|
||||
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
/**
|
||||
*
|
||||
* UserAgent provider for git related clients.
|
||||
* @author Sebastian Sdorra <sebastian.sdorra@gmail.com>
|
||||
* @since 1.45
|
||||
*/
|
||||
@Extension
|
||||
public class GitUserAgentProvider implements UserAgentProvider
|
||||
{
|
||||
public class GitUserAgentProvider implements UserAgentProvider {
|
||||
|
||||
private static final String PREFIX_JGIT = "jgit/";
|
||||
|
||||
/** Field description */
|
||||
@VisibleForTesting
|
||||
static final UserAgent GIT = UserAgent.builder("Git").browser(
|
||||
false).basicAuthenticationCharset(
|
||||
Charsets.UTF_8).build();
|
||||
|
||||
/** Field description */
|
||||
static final UserAgent JGIT = UserAgent.builder("JGit")
|
||||
.browser(false)
|
||||
.basicAuthenticationCharset(Charsets.UTF_8)
|
||||
.build();
|
||||
|
||||
private static final String PREFIX_REGULAR = "git/";
|
||||
|
||||
@VisibleForTesting
|
||||
static final UserAgent MSYSGIT = UserAgent.builder("msysGit").browser(
|
||||
false).basicAuthenticationCharset(
|
||||
Charsets.UTF_8).build();
|
||||
static final UserAgent GIT = UserAgent.builder("Git")
|
||||
.browser(false)
|
||||
.basicAuthenticationCharset(Charsets.UTF_8)
|
||||
.build();
|
||||
|
||||
private static final String PREFIX_LFS = "git-lfs/";
|
||||
|
||||
@VisibleForTesting
|
||||
static final UserAgent GIT_LFS = UserAgent.builder("Git Lfs")
|
||||
.browser(false)
|
||||
.basicAuthenticationCharset(Charsets.UTF_8)
|
||||
.build();
|
||||
|
||||
private static final String SUFFIX_MSYSGIT = "msysgit";
|
||||
|
||||
@VisibleForTesting
|
||||
static final UserAgent MSYSGIT = UserAgent.builder("msysGit")
|
||||
.browser(false)
|
||||
.basicAuthenticationCharset(Charsets.UTF_8)
|
||||
.build();
|
||||
|
||||
/** Field description */
|
||||
private static final String PREFIX = "git/";
|
||||
|
||||
/** Field description */
|
||||
private static final String SUFFIX = "msysgit";
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param userAgentString
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public UserAgent parseUserAgent(String userAgentString)
|
||||
{
|
||||
UserAgent ua = null;
|
||||
|
||||
if (userAgentString.startsWith(PREFIX))
|
||||
{
|
||||
if (userAgentString.contains(SUFFIX))
|
||||
{
|
||||
ua = MSYSGIT;
|
||||
}
|
||||
else
|
||||
{
|
||||
ua = GIT;
|
||||
}
|
||||
public UserAgent parseUserAgent(String userAgentString) {
|
||||
String lowerUserAgent = toLower(userAgentString);
|
||||
|
||||
if (isJGit(lowerUserAgent)) {
|
||||
return JGIT;
|
||||
} else if (isMsysGit(lowerUserAgent)) {
|
||||
return MSYSGIT;
|
||||
} else if (isGitLFS(lowerUserAgent)) {
|
||||
return GIT_LFS;
|
||||
} else if (isGit(lowerUserAgent)) {
|
||||
return GIT;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ua;
|
||||
}
|
||||
|
||||
private String toLower(String value) {
|
||||
return Strings.nullToEmpty(value).toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
|
||||
private boolean isJGit(String userAgent) {
|
||||
return userAgent.startsWith(PREFIX_JGIT);
|
||||
}
|
||||
|
||||
private boolean isMsysGit(String userAgent) {
|
||||
return userAgent.startsWith(PREFIX_REGULAR) && userAgent.contains(SUFFIX_MSYSGIT);
|
||||
}
|
||||
|
||||
private boolean isGitLFS(String userAgent) {
|
||||
return userAgent.startsWith(PREFIX_LFS);
|
||||
}
|
||||
|
||||
private boolean isGit(String userAgent) {
|
||||
return userAgent.startsWith(PREFIX_REGULAR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,23 +35,32 @@ package sonia.scm.web;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import org.eclipse.jgit.http.server.GitServlet;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import static org.slf4j.LoggerFactory.getLogger;
|
||||
|
||||
|
||||
import org.eclipse.jgit.lfs.lib.Constants;
|
||||
import static org.eclipse.jgit.lfs.lib.Constants.CONTENT_TYPE_GIT_LFS_JSON;
|
||||
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryProvider;
|
||||
import sonia.scm.repository.RepositoryRequestListenerUtil;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.web.lfs.servlet.LfsServletFactory;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import sonia.scm.repository.RepositoryException;
|
||||
@@ -65,15 +74,15 @@ public class ScmGitServlet extends GitServlet
|
||||
{
|
||||
|
||||
/** Field description */
|
||||
public static final String REGEX_GITHTTPBACKEND =
|
||||
"(?x)^/git/(.*/(HEAD|info/refs|objects/(info/[^/]+|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\\.(pack|idx))|git-(upload|receive)-pack))$";
|
||||
public static final Pattern REGEX_GITHTTPBACKEND = Pattern.compile(
|
||||
"(?x)^/git/(.*/(HEAD|info/refs|objects/(info/[^/]+|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\\.(pack|idx))|git-(upload|receive)-pack))$"
|
||||
);
|
||||
|
||||
/** Field description */
|
||||
private static final long serialVersionUID = -7712897339207470674L;
|
||||
|
||||
/** the logger for ScmGitServlet */
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(ScmGitServlet.class);
|
||||
private static final Logger logger = getLogger(ScmGitServlet.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
@@ -87,17 +96,21 @@ public class ScmGitServlet extends GitServlet
|
||||
* @param repositoryViewer
|
||||
* @param repositoryProvider
|
||||
* @param repositoryRequestListenerUtil
|
||||
* @param lfsServletFactory
|
||||
*/
|
||||
@Inject
|
||||
public ScmGitServlet(GitRepositoryResolver repositoryResolver,
|
||||
GitReceivePackFactory receivePackFactory,
|
||||
GitRepositoryViewer repositoryViewer,
|
||||
RepositoryProvider repositoryProvider,
|
||||
RepositoryRequestListenerUtil repositoryRequestListenerUtil)
|
||||
GitReceivePackFactory receivePackFactory,
|
||||
GitRepositoryViewer repositoryViewer,
|
||||
RepositoryProvider repositoryProvider,
|
||||
RepositoryRequestListenerUtil repositoryRequestListenerUtil,
|
||||
LfsServletFactory lfsServletFactory)
|
||||
{
|
||||
this.repositoryProvider = repositoryProvider;
|
||||
this.repositoryViewer = repositoryViewer;
|
||||
this.repositoryRequestListenerUtil = repositoryRequestListenerUtil;
|
||||
this.lfsServletFactory = lfsServletFactory;
|
||||
|
||||
setRepositoryResolver(repositoryResolver);
|
||||
setReceivePackFactory(receivePackFactory);
|
||||
}
|
||||
@@ -118,74 +131,165 @@ public class ScmGitServlet extends GitServlet
|
||||
protected void service(HttpServletRequest request,
|
||||
HttpServletResponse response)
|
||||
throws ServletException, IOException
|
||||
{
|
||||
String uri = HttpUtil.getStrippedURI(request);
|
||||
|
||||
if (uri.matches(REGEX_GITHTTPBACKEND))
|
||||
{
|
||||
sonia.scm.repository.Repository repository = repositoryProvider.get();
|
||||
|
||||
if (repository != null)
|
||||
{
|
||||
if (repositoryRequestListenerUtil.callListeners(request, response,
|
||||
repository))
|
||||
{
|
||||
super.service(request, response);
|
||||
}
|
||||
else if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("request aborted by repository request listener");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
super.service(request, response);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
printGitInformation(request, response);
|
||||
{
|
||||
Repository repository = repositoryProvider.get();
|
||||
if (repository != null) {
|
||||
handleRequest(request, response, repository);
|
||||
} else {
|
||||
// logger
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides the type request being currently made and delegates it accordingly.
|
||||
* <ul>
|
||||
* <li>Batch API:</li>
|
||||
* <ul>
|
||||
* <li>used to provide the client with information on how handle the large files of a repository.</li>
|
||||
* <li>response contains the information where to perform the actual upload and download of the large objects.</li>
|
||||
* </ul>
|
||||
* <li>Transfer API:</li>
|
||||
* <ul>
|
||||
* <li>receives and provides the actual large objects (resolves the pointer placed in the file of the working copy).</li>
|
||||
* <li>invoked only after the Batch API has been questioned about what to do with the large files</li>
|
||||
* </ul>
|
||||
* <li>Regular Git Http API:</li>
|
||||
* <ul>
|
||||
* <li>regular git http wire protocol, use by normal git clients.</li>
|
||||
* </ul>
|
||||
* <li>Browser Overview:<li>
|
||||
* <ul>
|
||||
* <li>short repository overview for browser clients.</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
private void handleRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
|
||||
logger.trace("handle git repository at {}", repository.getName());
|
||||
if (isLfsBatchApiRequest(request, repository.getName())) {
|
||||
HttpServlet servlet = lfsServletFactory.createProtocolServletFor(repository, request);
|
||||
logger.trace("handle lfs batch api request");
|
||||
handleGitLfsRequest(servlet, request, response, repository);
|
||||
} else if (isLfsFileTransferRequest(request, repository.getName())) {
|
||||
HttpServlet servlet = lfsServletFactory.createFileLfsServletFor(repository, request);
|
||||
logger.trace("handle lfs file transfer request");
|
||||
handleGitLfsRequest(servlet, request, response, repository);
|
||||
} else if (isRegularGitAPIRequest(request)) {
|
||||
logger.trace("handle regular git request");
|
||||
// continue with the regular git Backend
|
||||
handleRegularGitRequest(request, response, repository);
|
||||
} else {
|
||||
logger.trace("handle browser request");
|
||||
handleBrowserRequest(request, response, repository);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRegularGitAPIRequest(HttpServletRequest request) {
|
||||
return REGEX_GITHTTPBACKEND.matcher(HttpUtil.getStrippedURI(request)).matches();
|
||||
}
|
||||
|
||||
private void handleGitLfsRequest(HttpServlet servlet, HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
|
||||
if (repositoryRequestListenerUtil.callListeners(request, response, repository)) {
|
||||
servlet.service(request, response);
|
||||
} else if (logger.isDebugEnabled()) {
|
||||
logger.debug("request aborted by repository request listener");
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRegularGitRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
|
||||
if (repositoryRequestListenerUtil.callListeners(request, response, repository)) {
|
||||
super.service(request, response);
|
||||
} else if (logger.isDebugEnabled()) {
|
||||
logger.debug("request aborted by repository request listener");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method renders basic information about the repository into the response. The result is meant to be viewed by
|
||||
* browser.
|
||||
* @param request
|
||||
* @param response
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws ServletException
|
||||
*/
|
||||
private void printGitInformation(HttpServletRequest request,
|
||||
HttpServletResponse response)
|
||||
throws ServletException, IOException
|
||||
{
|
||||
sonia.scm.repository.Repository scmRepository = repositoryProvider.get();
|
||||
|
||||
if (scmRepository != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
repositoryViewer.handleRequest(request, response, scmRepository);
|
||||
}
|
||||
catch (RepositoryException ex)
|
||||
{
|
||||
throw new ServletException("could not create repository view", ex);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
throw new ServletException("could not create repository view", ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
private void handleBrowserRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
|
||||
try {
|
||||
repositoryViewer.handleRequest(request, response, repository);
|
||||
} catch (RepositoryException | IOException ex) {
|
||||
throw new ServletException("could not create repository view", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides whether or not a request is for the LFS Batch API,
|
||||
* <p>
|
||||
* - PUT or GET
|
||||
* - exactly for this repository
|
||||
* - Content Type is {@link Constants#HDR_APPLICATION_OCTET_STREAM}.
|
||||
*
|
||||
* @return Returns {@code false} if either of the conditions does not match. Returns true if all match.
|
||||
*/
|
||||
private static boolean isLfsFileTransferRequest(HttpServletRequest request, String repository) {
|
||||
|
||||
String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/[a-z0-9]{64}$", request.getContextPath(), GitServletModule.GIT_PATH, repository);
|
||||
boolean pathMatches = request.getRequestURI().matches(regex);
|
||||
|
||||
boolean methodMatches = request.getMethod().equals("PUT") || request.getMethod().equals("GET");
|
||||
|
||||
return pathMatches && methodMatches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides whether or not a request is for the LFS Batch API,
|
||||
* <p>
|
||||
* - POST
|
||||
* - exactly for this repository
|
||||
* - Content Type is {@link Constants#CONTENT_TYPE_GIT_LFS_JSON}.
|
||||
*
|
||||
* @return Returns {@code false} if either of the conditions does not match. Returns true if all match.
|
||||
*/
|
||||
private static boolean isLfsBatchApiRequest(HttpServletRequest request, String repository) {
|
||||
|
||||
String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/batch$", request.getContextPath(), GitServletModule.GIT_PATH, repository);
|
||||
boolean pathMatches = request.getRequestURI().matches(regex);
|
||||
|
||||
boolean methodMatches = "POST".equals(request.getMethod());
|
||||
|
||||
boolean headerContentTypeMatches = isLfsContentHeaderField(request.getContentType(), CONTENT_TYPE_GIT_LFS_JSON);
|
||||
boolean headerAcceptMatches = isLfsContentHeaderField(request.getHeader("Accept"), CONTENT_TYPE_GIT_LFS_JSON);
|
||||
|
||||
return pathMatches && methodMatches && headerContentTypeMatches && headerAcceptMatches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether request is of the specific content type.
|
||||
*
|
||||
* @param request The HTTP request header value to be examined.
|
||||
* @param expectedContentType The expected content type.
|
||||
* @return Returns {@code true} if the request has the expected content type. Return {@code false} otherwise.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static boolean isLfsContentHeaderField(String request, String expectedContentType) {
|
||||
|
||||
if (request == null || request.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] parts = request.split(" ");
|
||||
for (String part : parts) {
|
||||
if (part.startsWith(expectedContentType)) {
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
@@ -194,6 +298,11 @@ public class ScmGitServlet extends GitServlet
|
||||
/** Field description */
|
||||
private final RepositoryRequestListenerUtil repositoryRequestListenerUtil;
|
||||
|
||||
/** Field description */
|
||||
/**
|
||||
* Field description
|
||||
*/
|
||||
private final GitRepositoryViewer repositoryViewer;
|
||||
|
||||
private final LfsServletFactory lfsServletFactory;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.web.lfs;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.store.BlobStore;
|
||||
import sonia.scm.store.BlobStoreFactory;
|
||||
|
||||
/**
|
||||
* Creates {@link BlobStore} objects to store lfs objects.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.54
|
||||
*/
|
||||
@Singleton
|
||||
public class LfsBlobStoreFactory {
|
||||
|
||||
private static final String GIT_LFS_REPOSITORY_POSTFIX = "-git-lfs";
|
||||
|
||||
private final BlobStoreFactory blobStoreFactory;
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param blobStoreFactory blob store factory
|
||||
*/
|
||||
@Inject
|
||||
public LfsBlobStoreFactory(BlobStoreFactory blobStoreFactory) {
|
||||
this.blobStoreFactory = blobStoreFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a {@link BlobStore} corresponding to the SCM Repository.
|
||||
* <p>
|
||||
* git-lfs repositories should generally carry the same name as their regular SCM repository counterparts. However,
|
||||
* we have decided to store them under their IDs instead of their names, since the names might change and provide
|
||||
* other drawbacks, as well.
|
||||
* <p>
|
||||
* These repositories will have {@linkplain #GIT_LFS_REPOSITORY_POSTFIX} appended to their IDs.
|
||||
*
|
||||
* @param repository The SCM Repository to provide a LFS {@link BlobStore} for.
|
||||
*
|
||||
* @return blob store for the corresponding scm repository
|
||||
*/
|
||||
public BlobStore getLfsBlobStore(Repository repository) {
|
||||
return blobStoreFactory.getBlobStore(repository.getId() + GIT_LFS_REPOSITORY_POSTFIX);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package sonia.scm.web.lfs.servlet;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.gson.FieldNamingPolicy;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.eclipse.jgit.lfs.errors.CorruptLongObjectException;
|
||||
import org.eclipse.jgit.lfs.errors.InvalidLongObjectIdException;
|
||||
import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
|
||||
import org.eclipse.jgit.lfs.lib.Constants;
|
||||
import org.eclipse.jgit.lfs.lib.LongObjectId;
|
||||
import org.eclipse.jgit.lfs.server.LfsProtocolServlet;
|
||||
import org.eclipse.jgit.lfs.server.fs.FileLfsServlet;
|
||||
import org.eclipse.jgit.lfs.server.internal.LfsServerText;
|
||||
import org.eclipse.jgit.util.HttpSupport;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.store.Blob;
|
||||
import sonia.scm.store.BlobStore;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.MessageFormat;
|
||||
|
||||
/**
|
||||
* This Servlet provides the upload and download of files via git-lfs.
|
||||
* <p>
|
||||
* This implementation is based on {@link FileLfsServlet} but adjusted to work with
|
||||
* servlet-2.5 instead of servlet-3.1.
|
||||
* <p>
|
||||
*
|
||||
* @see FileLfsServlet
|
||||
* @since 1.54
|
||||
* Created by omilke on 15.05.2017.
|
||||
*/
|
||||
public class ScmFileTransferServlet extends HttpServlet {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ScmFileTransferServlet.class);
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Gson is used because the implementation was based on the jgit implementation. However the {@link LfsProtocolServlet} (which we do use in
|
||||
* {@link ScmLfsProtocolServlet}) also uses Gson, which currently ties us to Gson anyway.
|
||||
*/
|
||||
private static Gson gson = createGson();
|
||||
|
||||
private final BlobStore blobStore;
|
||||
|
||||
public ScmFileTransferServlet(BlobStore store) {
|
||||
|
||||
this.blobStore = store;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extracts the part after the last slash from path.
|
||||
*
|
||||
* @return Returns {@code null} if the part after the last slash is itself {@code null} or if its length is not 64.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static String objectIdFromPath(String info) {
|
||||
|
||||
int lastSlash = info.lastIndexOf('/');
|
||||
String potentialObjectId = info.substring(lastSlash + 1);
|
||||
|
||||
if (potentialObjectId.length() != 64) {
|
||||
return null;
|
||||
|
||||
} else {
|
||||
return potentialObjectId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the message and provides it to the client.
|
||||
*
|
||||
* @param response The response
|
||||
* @param status The HTTP Status Code to be provided to the client.
|
||||
* @param message the message to used for server-side logging. It is also provided to the client.
|
||||
*/
|
||||
private static void sendErrorAndLog(HttpServletResponse response, int status, String message) throws IOException {
|
||||
|
||||
logger.warn("Error occurred during git-lfs file transfer: {}", message);
|
||||
|
||||
sendError(response, status, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the exception and provides only the message of the exception to the client.
|
||||
*
|
||||
* @param response The response
|
||||
* @param status The HTTP Status Code to be provided to the client.
|
||||
* @param exception An exception to used for server-side logging.
|
||||
*/
|
||||
private static void sendErrorAndLog(HttpServletResponse response, int status, Exception exception) throws IOException {
|
||||
|
||||
logger.warn("Error occurred during git-lfs file transfer.", exception);
|
||||
String message = exception.getMessage();
|
||||
|
||||
|
||||
sendError(response, status, message);
|
||||
}
|
||||
|
||||
private static void sendError(HttpServletResponse response, int status, String message) throws IOException {
|
||||
|
||||
try (PrintWriter writer = response.getWriter()) {
|
||||
|
||||
gson.toJson(new Error(message), writer);
|
||||
|
||||
response.setStatus(status);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
response.flushBuffer();
|
||||
}
|
||||
|
||||
private static Gson createGson() {
|
||||
|
||||
GsonBuilder gb = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setPrettyPrinting().disableHtmlEscaping();
|
||||
return gb.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a blob to download.
|
||||
* <p>
|
||||
* Actual implementation is based on <code>org.eclipse.jgit.lfs.server.fs.ObjectDownloadListener</code> and adjusted
|
||||
* to non-async as we're currently on servlet-2.5.
|
||||
*
|
||||
* @param request servlet request
|
||||
* @param response servlet response
|
||||
* @throws ServletException if a servlet-specific error occurs
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
|
||||
AnyLongObjectId objectId = getObjectToTransfer(request, response);
|
||||
if (objectId == null) {
|
||||
|
||||
logInvalidObjectId(request.getRequestURI());
|
||||
} else {
|
||||
|
||||
final String objectIdName = objectId.getName();
|
||||
logger.trace("---- providing download for LFS-Oid: {}", objectIdName);
|
||||
|
||||
Blob savedBlob = blobStore.get(objectIdName);
|
||||
if (isBlobPresent(savedBlob)) {
|
||||
|
||||
logger.trace("----- Object {}: providing {} bytes", objectIdName, savedBlob.getSize());
|
||||
writeBlobIntoResponse(savedBlob, response);
|
||||
} else {
|
||||
|
||||
sendErrorAndLog(response, HttpStatus.SC_NOT_FOUND, MessageFormat.format(LfsServerText.get().objectNotFound, objectIdName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a blob from an upload.
|
||||
* <p>
|
||||
* Actual implementation is based on <code>org.eclipse.jgit.lfs.server.fs.ObjectUploadListener</code> and adjusted
|
||||
* to non-async as we're currently on servlet-2.5.
|
||||
*
|
||||
* @param request servlet request
|
||||
* @param response servlet response
|
||||
* @throws ServletException if a servlet-specific error occurs
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
|
||||
AnyLongObjectId objectId = getObjectToTransfer(request, response);
|
||||
if (objectId == null) {
|
||||
|
||||
logInvalidObjectId(request.getRequestURI());
|
||||
} else {
|
||||
|
||||
logger.trace("---- receiving upload for LFS-Oid: {}", objectId.getName());
|
||||
readBlobFromResponse(request, response, objectId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the {@link LongObjectId} from the request. Finishes the request, in case the {@link LongObjectId} cannot
|
||||
* be extracted with an appropriate error.
|
||||
*
|
||||
* @throws IOException Thrown if the response could not be completed in an error case.
|
||||
*/
|
||||
private AnyLongObjectId getObjectToTransfer(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
|
||||
String path = request.getPathInfo();
|
||||
|
||||
String objectIdFromPath = objectIdFromPath(path);
|
||||
if (objectIdFromPath == null) {
|
||||
|
||||
//ObjectId is not retrievable from URL
|
||||
sendErrorAndLog(response, HttpStatus.SC_UNPROCESSABLE_ENTITY, MessageFormat.format(LfsServerText.get().invalidPathInfo, path));
|
||||
return null;
|
||||
} else {
|
||||
try {
|
||||
return LongObjectId.fromString(objectIdFromPath);
|
||||
} catch (InvalidLongObjectIdException e) {
|
||||
|
||||
sendErrorAndLog(response, HttpStatus.SC_UNPROCESSABLE_ENTITY, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void logInvalidObjectId(String requestURI) {
|
||||
|
||||
logger.warn("---- could not extract Oid from Request. Path seems to be invalid: {}", requestURI);
|
||||
}
|
||||
|
||||
private boolean isBlobPresent(Blob savedBlob) {
|
||||
|
||||
return savedBlob != null && savedBlob.getSize() >= 0;
|
||||
}
|
||||
|
||||
private void writeBlobIntoResponse(Blob savedBlob, HttpServletResponse response) throws IOException {
|
||||
|
||||
try (ServletOutputStream responseOutputStream = response.getOutputStream();
|
||||
InputStream savedBlobInputStream = savedBlob.getInputStream()) {
|
||||
|
||||
response.addHeader(HttpSupport.HDR_CONTENT_LENGTH, String.valueOf(savedBlob.getSize()));
|
||||
response.setContentType(Constants.HDR_APPLICATION_OCTET_STREAM);
|
||||
|
||||
IOUtil.copy(savedBlobInputStream, responseOutputStream);
|
||||
} catch (IOException ex) {
|
||||
|
||||
sendErrorAndLog(response, HttpStatus.SC_INTERNAL_SERVER_ERROR, ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void readBlobFromResponse(HttpServletRequest request, HttpServletResponse response, AnyLongObjectId objectId) throws IOException {
|
||||
|
||||
Blob blob = blobStore.create(objectId.getName());
|
||||
try (OutputStream blobOutputStream = blob.getOutputStream();
|
||||
ServletInputStream requestInputStream = request.getInputStream()) {
|
||||
|
||||
IOUtil.copy(requestInputStream, blobOutputStream);
|
||||
blob.commit();
|
||||
|
||||
response.setContentType(Constants.CONTENT_TYPE_GIT_LFS_JSON);
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
} catch (CorruptLongObjectException ex) {
|
||||
|
||||
sendErrorAndLog(response, HttpStatus.SC_BAD_REQUEST, ex);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for providing an error message.
|
||||
*/
|
||||
private static class Error {
|
||||
String message;
|
||||
|
||||
Error(String m) {
|
||||
|
||||
this.message = m;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ public class GitRepositoryClientProvider extends RepositoryClientProvider
|
||||
|
||||
@Override
|
||||
public File getWorkingCopy() {
|
||||
return git.getRepository().getDirectory();
|
||||
return git.getRepository().getWorkTree();
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,51 +33,46 @@ package sonia.scm.web;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
*
|
||||
* Unit tests for {@link GitUserAgentProvider}.
|
||||
*
|
||||
* @author Sebastian Sdorra <sebastian.sdorra@triology.de>
|
||||
*/
|
||||
public class GitUserAgentProviderTest
|
||||
{
|
||||
public class GitUserAgentProviderTest {
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
private final GitUserAgentProvider provider = new GitUserAgentProvider();
|
||||
|
||||
@Test
|
||||
public void testParseUserAgent()
|
||||
{
|
||||
public void testParseUserAgent() {
|
||||
assertEquals(GitUserAgentProvider.GIT, parse("git/1.7.9.5"));
|
||||
assertEquals(GitUserAgentProvider.JGIT, parse("jgit/4.5.2"));
|
||||
assertEquals(GitUserAgentProvider.GIT_LFS, parse("git-lfs/2.0.1 (GitHub; windows amd64; go 1.8; git 678cdbd4)"));
|
||||
assertEquals(GitUserAgentProvider.MSYSGIT, parse("git/1.8.3.msysgit.0"));
|
||||
assertNull(parse("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param v
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private UserAgent parse(String v)
|
||||
{
|
||||
return provider.parseUserAgent(
|
||||
Strings.nullToEmpty(v).toLowerCase(Locale.ENGLISH));
|
||||
@Test
|
||||
public void testParseUserAgentCaseSensitive() {
|
||||
assertEquals(GitUserAgentProvider.GIT, parse("Git/1.7.9.5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseUserAgentWithEmptyValue() {
|
||||
assertNull(parse(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseUserAgentWithNullValue() {
|
||||
assertNull(parse(null));
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private final GitUserAgentProvider provider = new GitUserAgentProvider();
|
||||
private UserAgent parse(String v) {
|
||||
return provider.parseUserAgent(v);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.web.lfs;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import java.util.List;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import static org.mockito.Mockito.*;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryEvent;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.store.Blob;
|
||||
import sonia.scm.store.BlobStore;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link LfsStoreRemoveListener}.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class LfsStoreRemoveListenerTest {
|
||||
|
||||
@Mock
|
||||
private LfsBlobStoreFactory lfsBlobStoreFactory;
|
||||
|
||||
@Mock
|
||||
private BlobStore blobStore;
|
||||
|
||||
@InjectMocks
|
||||
private LfsStoreRemoveListener lfsStoreRemoveListener;
|
||||
|
||||
@Test
|
||||
public void testHandleRepositoryEventWithNonDeleteEvents() {
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_CREATE));
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.CREATE));
|
||||
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_MODIFY));
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.MODIFY));
|
||||
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_DELETE));
|
||||
|
||||
verifyZeroInteractions(lfsBlobStoreFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleRepositoryEventWithNonGitRepositories() {
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "svn"));
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "hg"));
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "dummy"));
|
||||
|
||||
verifyZeroInteractions(lfsBlobStoreFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleRepositoryEvent() {
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold("git");
|
||||
|
||||
when(lfsBlobStoreFactory.getLfsBlobStore(heartOfGold)).thenReturn(blobStore);
|
||||
Blob blobA = mockBlob("a");
|
||||
Blob blobB = mockBlob("b");
|
||||
List<Blob> blobs = Lists.newArrayList(blobA, blobB);
|
||||
when(blobStore.getAll()).thenReturn(blobs);
|
||||
|
||||
|
||||
lfsStoreRemoveListener.handleRepositoryEvent(new RepositoryEvent(HandlerEventType.DELETE, heartOfGold));
|
||||
verify(blobStore).getAll();
|
||||
verify(blobStore).remove(blobA);
|
||||
verify(blobStore).remove(blobB);
|
||||
|
||||
verifyNoMoreInteractions(blobStore);
|
||||
}
|
||||
|
||||
private Blob mockBlob(String id) {
|
||||
Blob blob = mock(Blob.class);
|
||||
when(blob.getId()).thenReturn(id);
|
||||
return blob;
|
||||
}
|
||||
|
||||
private RepositoryEvent event(HandlerEventType eventType) {
|
||||
return event(eventType, "git");
|
||||
}
|
||||
|
||||
private RepositoryEvent event(HandlerEventType eventType, String repositoryType) {
|
||||
return new RepositoryEvent(eventType, RepositoryTestData.create42Puzzle(repositoryType));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
<dependency>
|
||||
<groupId>com.aragost.javahg</groupId>
|
||||
<artifactId>javahg</artifactId>
|
||||
<version>0.7-scm1</version>
|
||||
<version>0.8-scm1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.google.guava</groupId>
|
||||
|
||||
@@ -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'),
|
||||
])
|
||||
}
|
||||
@@ -172,7 +172,7 @@
|
||||
<plugin>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>nativepkg-maven-plugin</artifactId>
|
||||
<version>1.1.3</version>
|
||||
<version>1.1.4</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
@@ -204,6 +204,7 @@
|
||||
</platform>
|
||||
<scripts>
|
||||
<preInstall>${project.basedir}/src/main/nativepkg/create-user</preInstall>
|
||||
<postInstall>${project.basedir}/src/main/nativepkg/clear-cache</postInstall>
|
||||
</scripts>
|
||||
<mappings>
|
||||
<files>
|
||||
|
||||
9
scm-server/src/main/nativepkg/clear-cache
Normal file
9
scm-server/src/main/nativepkg/clear-cache
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
# clear workdir after upgrade
|
||||
# https://bitbucket.org/sdorra/scm-manager/issues/923/scmmanager-installed-from-debian-package
|
||||
|
||||
WORKDIR="/var/cache/scm/work/webapp"
|
||||
if [ -d "${WORKDIR}" ]; then
|
||||
rm -rf "${WORKDIR}"
|
||||
fi
|
||||
@@ -63,12 +63,6 @@
|
||||
<name>tmatesoft release repository</name>
|
||||
<url>https://maven.tmatesoft.com/content/repositories/releases</url>
|
||||
</repository>
|
||||
|
||||
<repository>
|
||||
<id>jgit-repository</id>
|
||||
<name>jgit release repository</name>
|
||||
<url>http://download.eclipse.org/jgit/maven</url>
|
||||
</repository>
|
||||
|
||||
</repositories>
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -218,20 +218,9 @@
|
||||
<!-- rest documentation -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.codehaus.enunciate</groupId>
|
||||
<artifactId>enunciate-jersey-rt</artifactId>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-core-annotations</artifactId>
|
||||
<version>${enunciate.version}</version>
|
||||
<!-- fix conflict with jersey-json -->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>jackson-jaxrs</artifactId>
|
||||
<groupId>org.codehaus.jackson</groupId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<artifactId>jackson-xc</artifactId>
|
||||
<groupId>org.codehaus.jackson</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- test scope -->
|
||||
@@ -545,10 +534,14 @@
|
||||
<scm.home>target/scm-it</scm.home>
|
||||
<environment.profile>default</environment.profile>
|
||||
<selenium.version>2.53.1</selenium.version>
|
||||
<enunciate.version>1.31</enunciate.version>
|
||||
<enunciate.version>2.9.1</enunciate.version>
|
||||
<wagon.version>1.0</wagon.version>
|
||||
<mustache.version>0.8.17</mustache.version>
|
||||
<netbeans.hint.deploy.server>Tomcat</netbeans.hint.deploy.server>
|
||||
<sonar.issue.ignore.multicriteria>e1</sonar.issue.ignore.multicriteria>
|
||||
<sonar.issue.ignore.multicriteria.e1.ruleKey>javascript:S3827</sonar.issue.ignore.multicriteria.e1.ruleKey>
|
||||
<sonar.issue.ignore.multicriteria.e1.resourceKey>**.js</sonar.issue.ignore.multicriteria.e1.resourceKey>
|
||||
<sonar.exclusions>src/main/webapp/resources/extjs/**,src/main/webapp/resources/moment/**,src/main/webapp/resources/syntaxhighlighter/**</sonar.exclusions>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
@@ -793,8 +786,35 @@
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.codehaus.enunciate</groupId>
|
||||
<artifactId>maven-enunciate-plugin</artifactId>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>2.6</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-enunciate-configuration</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/doc</directory>
|
||||
<filtering>true</filtering>
|
||||
<includes>
|
||||
<include>**/enunciate.xml</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-maven-plugin</artifactId>
|
||||
<version>${enunciate.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
@@ -805,14 +825,21 @@
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<configFile>src/main/doc/enunciate.xml</configFile>
|
||||
<docsDir>${project.build.directory}/restdocs</docsDir>
|
||||
<configFile>${project.build.directory}/enunciate.xml</configFile>
|
||||
<docsDir>${project.build.directory}</docsDir>
|
||||
<docsSubdir>restdocs</docsSubdir>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.enunciate</groupId>
|
||||
<artifactId>enunciate-jersey</artifactId>
|
||||
<version>${enunciate.version}</version>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-top</artifactId>
|
||||
<version>2.9.1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.webcohesion.enunciate</groupId>
|
||||
<artifactId>enunciate-swagger</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
|
||||
@@ -39,30 +39,33 @@
|
||||
Description: Enunciate configuration
|
||||
-->
|
||||
|
||||
<enunciate label="full" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://enunciate.codehaus.org/schemas/enunciate-1.27.xsd">
|
||||
<enunciate xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://enunciate.webcohesion.com/schemas/enunciate-2.9.0.xsd"
|
||||
slug="scm-manager" version="${project.version}">
|
||||
|
||||
<title>SCM-Manager API</title>
|
||||
|
||||
<description>
|
||||
<![CDATA[
|
||||
<h1>SCM-Manager API</h1>
|
||||
<p>This page describes the RESTful Web Service API of <a href="https://www.scm-manager.org">SCM-Manager</a> ${project.version}.</p>
|
||||
]]>
|
||||
</description>
|
||||
|
||||
<api-classes>
|
||||
<include pattern="sonia.scm.api.rest.resources.*" />
|
||||
<exclude pattern="sonia.scm.debug.DebugResource" />
|
||||
<exclude pattern="sonia.scm.api.rest.resources.ConfigurationResource" />
|
||||
<exclude pattern="sonia.scm.api.rest.resources.SupportResource" />
|
||||
<exclude pattern="sonia.scm.api.rest.resources.RepositoryRootResource" />
|
||||
</api-classes>
|
||||
|
||||
<services>
|
||||
<rest defaultRestSubcontext="/api/rest" />
|
||||
</services>
|
||||
|
||||
<modules>
|
||||
|
||||
<docs title="SCM-Manager API" />
|
||||
<jaxrs datatype-detection="local">
|
||||
<application path="/api/rest" />
|
||||
</jaxrs>
|
||||
|
||||
<jersey resourceProviderFactory="com.sun.jersey.guice.spi.container.GuiceComponentProviderFactory">
|
||||
<init-param name="com.sun.jersey.api.json.POJOMappingFeature" value="true" />
|
||||
<init-param name="com.sun.jersey.config.feature.Redirect" value="true" />
|
||||
<init-param name="com.sun.jersey.config.property.resourceConfigClass" value="sonia.scm.api.rest.UriExtensionsConfig" />
|
||||
<init-param name="com.sun.jersey.config.property.packages" value="sonia.scm.api.rest" />
|
||||
</jersey>
|
||||
<docs disableResourceLinks="true" includeApplicationPath="true" />
|
||||
|
||||
</modules>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -38,6 +38,10 @@ package sonia.scm.api.rest.resources;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
|
||||
import sonia.scm.api.rest.Permission;
|
||||
import sonia.scm.security.AssignedPermission;
|
||||
@@ -114,13 +118,7 @@ public abstract class AbstractPermissionResource
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Adds a new permission to the user or group managed by the resource.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>201 add successful</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Adds a new permission to the user or group managed by the resource.
|
||||
*
|
||||
* @param uriInfo uri informations
|
||||
* @param permission permission to add
|
||||
@@ -128,6 +126,13 @@ public abstract class AbstractPermissionResource
|
||||
* @return web response
|
||||
*/
|
||||
@POST
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "creates", additionalHeaders = {
|
||||
@ResponseHeader(name = "Location", description = "uri to new create permission")
|
||||
}),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Response add(@Context UriInfo uriInfo, Permission permission)
|
||||
{
|
||||
@@ -139,15 +144,7 @@ public abstract class AbstractPermissionResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a permission from the user or group managed by the resource.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 delete successful</li>
|
||||
* <li>400 bad request, permission id does not belong to the user or group</li>
|
||||
* <li>404 not found, no permission with the specified id available</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Deletes a permission from the user or group managed by the resource.
|
||||
*
|
||||
* @param id id of the permission
|
||||
*
|
||||
@@ -155,6 +152,13 @@ public abstract class AbstractPermissionResource
|
||||
*/
|
||||
@DELETE
|
||||
@Path("{id}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, permission id does not belong to the user or group"),
|
||||
@ResponseCode(code = 404, condition = "not found, no permission with the specified id available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
public Response delete(@PathParam("id") String id)
|
||||
{
|
||||
StoredAssignedPermission sap = getPermission(id);
|
||||
@@ -165,16 +169,7 @@ public abstract class AbstractPermissionResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the specified permission on the user or group managed by the
|
||||
* resource.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>204 update successful</li>
|
||||
* <li>400 bad request, permission id does not belong to the user or group</li>
|
||||
* <li>404 not found, no permission with the specified id available</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Updates the specified permission on the user or group managed by the resource.
|
||||
*
|
||||
* @param id id of the permission
|
||||
* @param permission updated permission
|
||||
@@ -183,6 +178,13 @@ public abstract class AbstractPermissionResource
|
||||
*/
|
||||
@PUT
|
||||
@Path("{id}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, permission id does not belong to the user or group"),
|
||||
@ResponseCode(code = 404, condition = "not found, no permission with the specified id available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Response update(@PathParam("id") String id, Permission permission)
|
||||
{
|
||||
@@ -197,16 +199,7 @@ public abstract class AbstractPermissionResource
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the {@link Permission} with the specified id.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>400 bad request, permission id does not belong to the user or group</li>
|
||||
* <li>404 not found, no permission with the specified id available</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
*
|
||||
* Returns the {@link Permission} with the specified id.
|
||||
*
|
||||
* @param id id of the {@link Permission}
|
||||
*
|
||||
@@ -214,6 +207,12 @@ public abstract class AbstractPermissionResource
|
||||
*/
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, permission id does not belong to the user or group"),
|
||||
@ResponseCode(code = 404, condition = "not found, no permission with the specified id available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Permission get(@PathParam("id") String id)
|
||||
{
|
||||
@@ -223,17 +222,15 @@ public abstract class AbstractPermissionResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all permissions of the user or group managed by the resource.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns all permissions of the user or group managed by the resource.
|
||||
*
|
||||
* @return all permissions of the user or group
|
||||
*/
|
||||
@GET
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public List<Permission> getAll()
|
||||
{
|
||||
|
||||
@@ -39,7 +39,10 @@ import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import java.util.List;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
@@ -47,8 +50,6 @@ import org.apache.shiro.authc.DisabledAccountException;
|
||||
import org.apache.shiro.authc.ExcessiveAttemptsException;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
|
||||
import org.codehaus.enunciate.jaxrs.TypeHint;
|
||||
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -59,6 +60,11 @@ import sonia.scm.api.rest.RestActionResult;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.security.Tokens;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@@ -81,12 +87,12 @@ import sonia.scm.security.AccessTokenCookieIssuer;
|
||||
import sonia.scm.security.Scope;
|
||||
|
||||
/**
|
||||
*
|
||||
* Authentication related RESTful Web Service endpoint.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@Singleton
|
||||
@Path("auth")
|
||||
@ExternallyManagedLifecycle
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public class AuthenticationResource
|
||||
{
|
||||
@@ -128,15 +134,8 @@ public class AuthenticationResource
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Authenticate a user and return the state of the application.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>400 bad request, required parameter is missing.</li>
|
||||
* <li>401 unauthorized, the specified username or password is wrong</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
*
|
||||
* Authenticate a user and return the state of the application.
|
||||
*
|
||||
* @param request current http request
|
||||
* @param response current http response
|
||||
* @param grantType grant type, currently only password is supported
|
||||
@@ -150,6 +149,12 @@ public class AuthenticationResource
|
||||
@POST
|
||||
@Path("access_token")
|
||||
@TypeHint(ScmState.class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, required parameter is missing"),
|
||||
@ResponseCode(code = 401, condition = "unauthorized, the specified username or password is wrong"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response authenticate(
|
||||
@Context HttpServletRequest request,
|
||||
@Context HttpServletResponse response,
|
||||
@@ -238,13 +243,7 @@ public class AuthenticationResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout the current user. Returns the current state of the application,
|
||||
* if public access is enabled.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Logout the current user. Returns the current state of the application, if public access is enabled.
|
||||
*
|
||||
* @param request the current http request
|
||||
* @param response the current http response
|
||||
@@ -254,6 +253,10 @@ public class AuthenticationResource
|
||||
@GET
|
||||
@Path("logout")
|
||||
@TypeHint(ScmState.class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response logout(@Context HttpServletRequest request, @Context HttpServletResponse response)
|
||||
{
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
@@ -280,16 +283,8 @@ public class AuthenticationResource
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This method is an alias of the
|
||||
* {@link #getState(javax.servlet.http.HttpServletRequest)} method.
|
||||
* The only difference between the methods,
|
||||
* is that this one could not be used with basic authentication.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>401 unauthorized, user is not authenticated and public access is disabled.</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* This method is an alias of the {@link #getState(HttpServletRequest)} method.
|
||||
* The only difference between the methods, is that this one could not be used with basic authentication.
|
||||
*
|
||||
* @param request the current http request
|
||||
*
|
||||
@@ -298,19 +293,18 @@ public class AuthenticationResource
|
||||
@GET
|
||||
@Path("state")
|
||||
@TypeHint(ScmState.class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 401, condition = "unauthorized, user is not authenticated and public access is disabled"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response getCurrentState(@Context HttpServletRequest request)
|
||||
{
|
||||
return getState(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current state of the application.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>401 unauthorized, user is not authenticated and public access is disabled.</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns the current state of the application.
|
||||
*
|
||||
* @param request the current http request
|
||||
*
|
||||
@@ -318,6 +312,11 @@ public class AuthenticationResource
|
||||
*/
|
||||
@GET
|
||||
@TypeHint(ScmState.class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 401, condition = "unauthorized, user is not authenticated and public access is disabled"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response getState(@Context HttpServletRequest request)
|
||||
{
|
||||
Response response;
|
||||
|
||||
@@ -36,14 +36,14 @@ package sonia.scm.api.rest.resources;
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authc.credential.PasswordService;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
|
||||
import org.codehaus.enunciate.jaxrs.TypeHint;
|
||||
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -68,10 +68,10 @@ import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
/**
|
||||
*
|
||||
* Resource to change the password of the authenticated user.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@ExternallyManagedLifecycle
|
||||
@Path("action/change-password")
|
||||
public class ChangePasswordResource
|
||||
{
|
||||
@@ -100,14 +100,7 @@ public class ChangePasswordResource
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Changes the password of the current user.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>400 bad request, the old password is not correct</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Changes the password of the current user.
|
||||
*
|
||||
* @param oldPassword old password of the current user
|
||||
* @param newPassword new password for the current user
|
||||
@@ -119,6 +112,11 @@ public class ChangePasswordResource
|
||||
*/
|
||||
@POST
|
||||
@TypeHint(RestActionResult.class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, the old password is not correct"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Response changePassword(@FormParam("old-password") String oldPassword,
|
||||
@FormParam("new-password") String newPassword)
|
||||
|
||||
@@ -35,6 +35,8 @@ package sonia.scm.api.rest.resources;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
|
||||
@@ -60,12 +62,7 @@ public class CipherResource
|
||||
|
||||
/**
|
||||
* Encrypts the request body and returns an encrypted string. This method can
|
||||
* only executed with administration privileges.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* only executed with administration privileges.
|
||||
*
|
||||
* @param value value to encrypt
|
||||
*
|
||||
@@ -73,6 +70,10 @@ public class CipherResource
|
||||
*/
|
||||
@POST
|
||||
@Path("encrypt")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
public String encrypt(String value)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
|
||||
@@ -37,12 +37,13 @@ package sonia.scm.api.rest.resources;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
|
||||
import org.codehaus.enunciate.jaxrs.TypeHint;
|
||||
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
|
||||
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupException;
|
||||
import sonia.scm.group.GroupManager;
|
||||
@@ -70,12 +71,12 @@ import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
/**
|
||||
*
|
||||
* RESTful Web Service Resource to manage groups and their members.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@Path("groups")
|
||||
@Singleton
|
||||
@ExternallyManagedLifecycle
|
||||
public class GroupResource
|
||||
extends AbstractManagerResource<Group, GroupException>
|
||||
{
|
||||
@@ -102,15 +103,7 @@ public class GroupResource
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a new group.<br />
|
||||
* This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>201 create success</li>
|
||||
* <li>403 forbidden, the current user has no admin privileges</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Creates a new group. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param uriInfo current uri informations
|
||||
* @param group the group to be created
|
||||
@@ -118,6 +111,14 @@ public class GroupResource
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "create success", additionalHeaders = {
|
||||
@ResponseHeader(name = "Location", description = "uri to the created group")
|
||||
}),
|
||||
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
@Override
|
||||
public Response create(@Context UriInfo uriInfo, Group group)
|
||||
@@ -126,15 +127,7 @@ public class GroupResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a group.<br />
|
||||
* This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>201 delete success</li>
|
||||
* <li>403 forbidden, the current user has no admin privileges</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Deletes a group. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param name the name of the group to delete.
|
||||
*
|
||||
@@ -142,6 +135,12 @@ public class GroupResource
|
||||
*/
|
||||
@DELETE
|
||||
@Path("{id}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "delete success"),
|
||||
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Override
|
||||
public Response delete(@PathParam("id") String name)
|
||||
{
|
||||
@@ -149,15 +148,7 @@ public class GroupResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the given group.<br />
|
||||
* This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>201 update successful</li>
|
||||
* <li>403 forbidden, the current user has no admin privileges</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Modifies the given group. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param uriInfo current uri informations
|
||||
* @param name name of the group to be modified
|
||||
@@ -167,6 +158,12 @@ public class GroupResource
|
||||
*/
|
||||
@PUT
|
||||
@Path("{id}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "update success"),
|
||||
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
@Override
|
||||
public Response update(@Context UriInfo uriInfo,
|
||||
@@ -178,16 +175,7 @@ public class GroupResource
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns a group.<br />
|
||||
* This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>403 forbidden, the current user has no admin privileges</li>
|
||||
* <li>404 not found, no group with the specified id/name available</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Fetches a group by its name or id. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param request the current request
|
||||
* @param id the id/name of the group
|
||||
@@ -197,6 +185,12 @@ public class GroupResource
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@TypeHint(Group.class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
|
||||
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
@Override
|
||||
public Response get(@Context Request request, @PathParam("id") String id)
|
||||
@@ -216,15 +210,7 @@ public class GroupResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all groups.<br />
|
||||
* This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>403 forbidden, the current user has no admin privileges</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns all groups. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param request the current request
|
||||
* @param start the start value for paging
|
||||
@@ -237,6 +223,11 @@ public class GroupResource
|
||||
@GET
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
@TypeHint(Group[].class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Override
|
||||
public Response getAll(@Context Request request, @DefaultValue("0")
|
||||
@QueryParam("start") int start, @DefaultValue("-1")
|
||||
@@ -249,14 +240,6 @@ public class GroupResource
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param items
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected GenericEntity<Collection<Group>> createGenericEntity(
|
||||
Collection<Group> items)
|
||||
@@ -267,26 +250,12 @@ public class GroupResource
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param group
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected String getId(Group group)
|
||||
{
|
||||
return group.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected String getPathPart()
|
||||
{
|
||||
|
||||
@@ -34,11 +34,11 @@ package sonia.scm.api.rest.resources;
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
|
||||
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
|
||||
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.security.Role;
|
||||
|
||||
@@ -56,7 +56,6 @@ import javax.ws.rs.core.MediaType;
|
||||
* @since 1.41
|
||||
*/
|
||||
@Path("security/key")
|
||||
@ExternallyManagedLifecycle
|
||||
public class KeyResource
|
||||
{
|
||||
|
||||
@@ -75,17 +74,15 @@ public class KeyResource
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Generates a unique key. This method can only executed with administration
|
||||
* privileges.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Generates a unique key. <strong>Note:</strong> This method can only executed with administration privileges.
|
||||
*
|
||||
* @return unique key
|
||||
*/
|
||||
@GET
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
public String generateKey()
|
||||
{
|
||||
|
||||
@@ -39,8 +39,6 @@ import com.google.common.collect.Lists;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -55,6 +53,9 @@ import sonia.scm.plugin.PluginManager;
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import com.sun.jersey.multipart.FormDataParam;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -75,12 +76,12 @@ import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
|
||||
/**
|
||||
*
|
||||
* RESTful Web Service Endpoint to manage plugins.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@Singleton
|
||||
@Path("plugins")
|
||||
@ExternallyManagedLifecycle
|
||||
public class PluginResource
|
||||
{
|
||||
|
||||
@@ -107,21 +108,21 @@ public class PluginResource
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Installs a plugin from a package.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>412 precondition failed</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Installs a plugin from a package.
|
||||
*
|
||||
* @param uploadedInputStream
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
@POST
|
||||
@Path("install-package")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 412, condition = "precondition failed"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Response install(
|
||||
@@ -153,35 +154,30 @@ public class PluginResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a plugin.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Installs a plugin.
|
||||
*
|
||||
* @param id id of the plugin to be installed
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Path("install/{id}")
|
||||
public Response install(@PathParam("id") String id)
|
||||
{
|
||||
pluginManager.install(id);
|
||||
|
||||
// TODO should return 204 no content
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a plugin from a package. This method is a workaround for ExtJS
|
||||
* file upload, which requires text/html as content-type.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>412 precondition failed</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* file upload, which requires text/html as content-type.
|
||||
*
|
||||
* @param uploadedInputStream
|
||||
* @return
|
||||
@@ -190,6 +186,11 @@ public class PluginResource
|
||||
*/
|
||||
@POST
|
||||
@Path("install-package.html")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 412, condition = "precondition failed"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public Response installFromUI(
|
||||
@@ -200,60 +201,62 @@ public class PluginResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls a plugin.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Uninstalls a plugin.
|
||||
*
|
||||
* @param id id of the plugin to be uninstalled
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Path("uninstall/{id}")
|
||||
public Response uninstall(@PathParam("id") String id)
|
||||
{
|
||||
pluginManager.uninstall(id);
|
||||
|
||||
// TODO should return 204 content
|
||||
// consider to do a uninstall with a delete
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a plugin.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Updates a plugin.
|
||||
*
|
||||
* @param id id of the plugin to be updated
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Path("update/{id}")
|
||||
public Response update(@PathParam("id") String id)
|
||||
{
|
||||
pluginManager.update(id);
|
||||
|
||||
// TODO should return 204 content
|
||||
// consider to do an update with a put
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns all plugins.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns all plugins.
|
||||
*
|
||||
* @return all plugins
|
||||
*/
|
||||
@GET
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Collection<PluginInformation> getAll()
|
||||
{
|
||||
@@ -261,17 +264,16 @@ public class PluginResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all available plugins.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns all available plugins.
|
||||
*
|
||||
* @return all available plugins
|
||||
*/
|
||||
@GET
|
||||
@Path("available")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Collection<PluginInformation> getAvailable()
|
||||
{
|
||||
@@ -279,17 +281,16 @@ public class PluginResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all plugins which are available for update.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns all plugins which are available for update.
|
||||
*
|
||||
* @return all plugins which are available for update
|
||||
*/
|
||||
@GET
|
||||
@Path("updates")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Collection<PluginInformation> getAvailableUpdates()
|
||||
{
|
||||
@@ -297,17 +298,16 @@ public class PluginResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all installed plugins.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns all installed plugins.
|
||||
*
|
||||
* @return all installed plugins
|
||||
*/
|
||||
@GET
|
||||
@Path("installed")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Collection<PluginInformation> getInstalled()
|
||||
{
|
||||
@@ -315,17 +315,16 @@ public class PluginResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all plugins for the overview.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns all plugins for the overview.
|
||||
*
|
||||
* @return all plugins for the overview
|
||||
*/
|
||||
@GET
|
||||
@Path("overview")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Collection<PluginInformation> getOverview()
|
||||
{
|
||||
|
||||
@@ -43,9 +43,6 @@ import com.google.inject.Inject;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
|
||||
import org.codehaus.enunciate.jaxrs.TypeHint;
|
||||
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -74,6 +71,10 @@ import static com.google.common.base.Preconditions.*;
|
||||
|
||||
import com.sun.jersey.api.client.ClientResponse.Status;
|
||||
import com.sun.jersey.multipart.FormDataParam;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -111,7 +112,6 @@ import javax.xml.bind.annotation.XmlRootElement;
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@Path("import/repositories")
|
||||
@ExternallyManagedLifecycle
|
||||
public class RepositoryImportResource
|
||||
{
|
||||
|
||||
@@ -142,17 +142,8 @@ public class RepositoryImportResource
|
||||
/**
|
||||
* Imports a repository type specific bundle. The bundle file is uploaded to
|
||||
* the server which is running scm-manager. After the upload has finished, the
|
||||
* bundle file is passed to the {@link UnbundleCommandBuilder}. This method
|
||||
* requires admin privileges.<br />
|
||||
*
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>201 created</li>
|
||||
* <li>400 bad request, the import bundle feature is not supported by this
|
||||
* type of repositories or the parameters are not valid.</li>
|
||||
* <li>500 internal server error</li>
|
||||
* <li>409 conflict, a repository with the name already exists.</li>
|
||||
* </ul>
|
||||
* bundle file is passed to the {@link UnbundleCommandBuilder}. <strong>Note:</strong> This method
|
||||
* requires admin privileges.
|
||||
*
|
||||
* @param uriInfo uri info
|
||||
* @param type repository type
|
||||
@@ -160,12 +151,23 @@ public class RepositoryImportResource
|
||||
* @param inputStream input bundle
|
||||
* @param compressed true if the bundle is gzip compressed
|
||||
*
|
||||
* @return empty response with location header which points to the imported
|
||||
* repository
|
||||
* @return empty response with location header which points to the imported repository
|
||||
* @since 1.43
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}/bundle")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "created", additionalHeaders = {
|
||||
@ResponseHeader(name = "Location", description = "uri to the imported repository")
|
||||
}),
|
||||
@ResponseCode(
|
||||
code = 400,
|
||||
condition = "bad request, the import bundle feature is not supported by this type of repositories or the parameters are not valid"
|
||||
),
|
||||
@ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Response importFromBundle(@Context UriInfo uriInfo,
|
||||
@PathParam("type") String type, @FormDataParam("name") String name,
|
||||
@@ -182,18 +184,8 @@ public class RepositoryImportResource
|
||||
* This method works exactly like
|
||||
* {@link #importFromBundle(UriInfo, String, String, InputStream)}, but this
|
||||
* method returns an html content-type. The method exists only for a
|
||||
* workaround of the javascript ui extjs. This method requires admin
|
||||
* privileges.<br />
|
||||
*
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>201 created</li>
|
||||
* <li>400 bad request, the import bundle feature is not supported by this
|
||||
* type of repositories or the parameters are not valid.</li>
|
||||
* <li>500 internal server error</li>
|
||||
* <li>409 conflict, a repository with the name already exists.</li>
|
||||
* </ul>
|
||||
*
|
||||
* workaround of the javascript ui extjs. <strong>Note:</strong> This method requires admin
|
||||
* privileges.
|
||||
*
|
||||
* @param type repository type
|
||||
* @param name name of the repository
|
||||
@@ -206,6 +198,16 @@ public class RepositoryImportResource
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}/bundle.html")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(
|
||||
code = 400,
|
||||
condition = "bad request, the import bundle feature is not supported by this type of repositories or the parameters are not valid"
|
||||
),
|
||||
@ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(RestActionUploadResult.class)
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public Response importFromBundleUI(@PathParam("type") String type,
|
||||
@@ -234,16 +236,7 @@ public class RepositoryImportResource
|
||||
* Imports a external repository which is accessible via url. The method can
|
||||
* only be used, if the repository type supports the {@link Command#PULL}. The
|
||||
* method will return a location header with the url to the imported
|
||||
* repository. This method requires admin privileges.<br />
|
||||
*
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>201 created</li>
|
||||
* <li>400 bad request, the import by url feature is not supported by this
|
||||
* type of repositories or the parameters are not valid.</li>
|
||||
* <li>409 conflict, a repository with the name already exists.</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* repository. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param uriInfo uri info
|
||||
* @param type repository type
|
||||
@@ -255,6 +248,18 @@ public class RepositoryImportResource
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}/url")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "created", additionalHeaders = {
|
||||
@ResponseHeader(name = "Location", description = "uri to the imported repository")
|
||||
}),
|
||||
@ResponseCode(
|
||||
code = 400,
|
||||
condition = "bad request, the import feature is not supported by this type of repositories or the parameters are not valid"
|
||||
),
|
||||
@ResponseCode(code = 409, condition = "conflict, a repository with the name already exists"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Response importFromUrl(@Context UriInfo uriInfo,
|
||||
@PathParam("type") String type, UrlImportRequest request)
|
||||
@@ -298,15 +303,7 @@ public class RepositoryImportResource
|
||||
|
||||
/**
|
||||
* Imports repositories of the given type from the configured repository
|
||||
* directory. This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 ok, successful</li>
|
||||
* <li>400 bad request, the import feature is not
|
||||
* supported by this type of repositories.</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* directory. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param type repository type
|
||||
*
|
||||
@@ -314,6 +311,14 @@ public class RepositoryImportResource
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(
|
||||
code = 400,
|
||||
condition = "bad request, the import feature is not supported by this type of repositories"
|
||||
),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(Repository[].class)
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Response importRepositories(@PathParam("type") String type)
|
||||
@@ -333,19 +338,19 @@ public class RepositoryImportResource
|
||||
|
||||
/**
|
||||
* Imports repositories of all supported types from the configured repository
|
||||
* directories. This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 ok, successful</li>
|
||||
* <li>400 bad request, the import feature is not
|
||||
* supported by this type of repositories.</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* directories. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @return imported repositories
|
||||
*/
|
||||
@POST
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(
|
||||
code = 400,
|
||||
condition = "bad request, the import feature is not supported by this type of repositories"
|
||||
),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(Repository[].class)
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Response importRepositories()
|
||||
@@ -371,15 +376,7 @@ public class RepositoryImportResource
|
||||
/**
|
||||
* Imports repositories of the given type from the configured repository
|
||||
* directory. Returns a list of successfully imported directories and a list
|
||||
* of failed directories. This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 ok, successful</li>
|
||||
* <li>400 bad request, the import feature is not
|
||||
* supported by this type of repositories.</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* of failed directories. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param type repository type
|
||||
*
|
||||
@@ -388,6 +385,14 @@ public class RepositoryImportResource
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}/directory")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(
|
||||
code = 400,
|
||||
condition = "bad request, the import feature is not supported by this type of repositories"
|
||||
),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(ImportResult.class)
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Response importRepositoriesFromDirectory(
|
||||
@@ -456,22 +461,20 @@ public class RepositoryImportResource
|
||||
|
||||
/**
|
||||
* Returns a list of repository types, which support the directory import
|
||||
* feature.
|
||||
*
|
||||
* This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 ok, successful</li>
|
||||
* <li>400 bad request, the import feature is not
|
||||
* supported by this type of repositories.</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* feature. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @return list of repository types
|
||||
*/
|
||||
@GET
|
||||
@TypeHint(Type[].class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(
|
||||
code = 400,
|
||||
condition = "bad request, the import feature is not supported by this type of repositories"
|
||||
),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Response getImportableTypes()
|
||||
{
|
||||
|
||||
@@ -38,12 +38,13 @@ package sonia.scm.api.rest.resources;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
|
||||
import org.codehaus.enunciate.jaxrs.TypeHint;
|
||||
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -103,14 +104,13 @@ import javax.ws.rs.core.UriInfo;
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
|
||||
/**
|
||||
*
|
||||
* Repository related RESTful Web Service Endpoint.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@Singleton
|
||||
@Path("repositories")
|
||||
@ExternallyManagedLifecycle
|
||||
public class RepositoryResource
|
||||
extends AbstractManagerResource<Repository, RepositoryException>
|
||||
public class RepositoryResource extends AbstractManagerResource<Repository, RepositoryException>
|
||||
{
|
||||
|
||||
/** Field description */
|
||||
@@ -147,22 +147,22 @@ public class RepositoryResource
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a new repository.<br />
|
||||
* This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>201 create success</li>
|
||||
* <li>403 forbidden, the current user has no admin privileges</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Creates a new repository.<strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param uriInfo current uri informations
|
||||
* @param repository the repository to be created
|
||||
*
|
||||
* @return
|
||||
* @return empty response with location header to the new repository
|
||||
*/
|
||||
@POST
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "success", additionalHeaders = {
|
||||
@ResponseHeader(name = "Location", description = "uri to the new created repository")
|
||||
}),
|
||||
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
@Override
|
||||
public Response create(@Context UriInfo uriInfo, Repository repository)
|
||||
@@ -171,19 +171,7 @@ public class RepositoryResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a repository.<br />
|
||||
* This method requires owner privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>201 delete success</li>
|
||||
* <li>403 forbidden, the current user has no owner privileges</li>
|
||||
* <li>
|
||||
* 412 forbidden, the repository is not archived,
|
||||
* this error occurs only with enabled repository archive.
|
||||
* </li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Deletes a repository. <strong>Note:</strong> This method requires owner privileges.
|
||||
*
|
||||
* @param id the id of the repository to delete.
|
||||
*
|
||||
@@ -191,6 +179,17 @@ public class RepositoryResource
|
||||
*/
|
||||
@DELETE
|
||||
@Path("{id}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "delete success"),
|
||||
@ResponseCode(code = 403, condition = "forbidden, the current user has no owner privileges"),
|
||||
@ResponseCode(code = 404, condition = "could not find repository"),
|
||||
@ResponseCode(
|
||||
code = 412,
|
||||
condition = "precondition failed, the repository is not archived, this error occurs only with enabled repository archive"
|
||||
),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Override
|
||||
public Response delete(@PathParam("id") String id)
|
||||
{
|
||||
@@ -232,20 +231,20 @@ public class RepositoryResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Re run repository health checks.<br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>201 re run success</li>
|
||||
* <li>403 forbidden, the current user has no owner privileges</li>
|
||||
* <li>404 could not find repository</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Re run repository health checks.
|
||||
*
|
||||
* @param id id of the repository
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "re run success"),
|
||||
@ResponseCode(code = 403, condition = "forbidden, the current user has no owner privileges"),
|
||||
@ResponseCode(code = 404, condition = "could not find repository"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Path("{id}/healthcheck")
|
||||
public Response runHealthChecks(@PathParam("id") String id)
|
||||
{
|
||||
@@ -254,6 +253,7 @@ public class RepositoryResource
|
||||
try
|
||||
{
|
||||
healthChecker.check(id);
|
||||
// TODO should return 204 instead of 200
|
||||
response = Response.ok().build();
|
||||
}
|
||||
catch (RepositoryNotFoundException ex)
|
||||
@@ -271,15 +271,7 @@ public class RepositoryResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the given repository.<br />
|
||||
* This method requires owner privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>201 update successful</li>
|
||||
* <li>403 forbidden, the current user has no owner privileges</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Modifies the given repository. <strong>Note:</strong> This method requires owner privileges.
|
||||
*
|
||||
* @param uriInfo current uri informations
|
||||
* @param id id of the repository to be modified
|
||||
@@ -289,10 +281,16 @@ public class RepositoryResource
|
||||
*/
|
||||
@PUT
|
||||
@Path("{id}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "update successful"),
|
||||
@ResponseCode(code = 403, condition = "forbidden, the current user has no owner privileges"),
|
||||
@ResponseCode(code = 404, condition = "could not find repository"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
@Override
|
||||
public Response update(@Context UriInfo uriInfo, @PathParam("id") String id,
|
||||
Repository repository)
|
||||
public Response update(@Context UriInfo uriInfo, @PathParam("id") String id, Repository repository)
|
||||
{
|
||||
return super.update(uriInfo, id, repository);
|
||||
}
|
||||
@@ -300,14 +298,7 @@ public class RepositoryResource
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the {@link Repository} with the specified id.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>404 not found, no repository with the specified id available</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns the {@link Repository} with the specified id.
|
||||
*
|
||||
* @param request the current request
|
||||
* @param id the id/name of the user
|
||||
@@ -317,6 +308,11 @@ public class RepositoryResource
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 404, condition = "not found, no repository with the specified id available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(Repository.class)
|
||||
@Override
|
||||
public Response get(@Context Request request, @PathParam("id") String id)
|
||||
@@ -325,13 +321,7 @@ public class RepositoryResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all repositories.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns all repositories.
|
||||
*
|
||||
* @param request the current request
|
||||
* @param start the start value for paging
|
||||
@@ -343,6 +333,10 @@ public class RepositoryResource
|
||||
*/
|
||||
@GET
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(Repository[].class)
|
||||
@Override
|
||||
public Response getAll(@Context Request request, @DefaultValue("0")
|
||||
@@ -355,16 +349,7 @@ public class RepositoryResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a annotate/blame view for the given path.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>400 bad request, the blame feature is not
|
||||
* supported by this type of repositories.</li>
|
||||
* <li>404 not found, if the repository or the path could not be found</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns a annotate/blame view for the given path.
|
||||
*
|
||||
* @param id the id of the repository
|
||||
* @param revision the revision of the file
|
||||
@@ -377,6 +362,12 @@ public class RepositoryResource
|
||||
*/
|
||||
@GET
|
||||
@Path("{id}/blame")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, the blame feature is not supported by this type of repositories."),
|
||||
@ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(BlameResult.class)
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Response getBlame(@PathParam("id") String id,
|
||||
@@ -430,16 +421,7 @@ public class RepositoryResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link Branches} of a repository.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>400 bad request, the content feature is not
|
||||
* supported by this type of repositories.</li>
|
||||
* <li>404 not found, if the repository or the path could not be found</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns all {@link Branches} of a repository.
|
||||
*
|
||||
* @param id the id of the repository
|
||||
*
|
||||
@@ -452,6 +434,14 @@ public class RepositoryResource
|
||||
*/
|
||||
@GET
|
||||
@Path("{id}/branches")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, the branch feature is not supported by this type of repositories."),
|
||||
@ResponseCode(code = 404, condition = "not found, the repository could not be found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(Branches.class)
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Response getBranches(@PathParam("id") String id)
|
||||
throws RepositoryException, IOException
|
||||
{
|
||||
@@ -490,16 +480,7 @@ public class RepositoryResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of folders and files for the given folder.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>400 bad request, the browse feature is not
|
||||
* supported by this type of repositories.</li>
|
||||
* <li>404 not found, if the repository or the path could not be found</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns a list of folders and files for the given folder.
|
||||
*
|
||||
* @param id the id of the repository
|
||||
* @param revision the revision of the file
|
||||
@@ -515,6 +496,12 @@ public class RepositoryResource
|
||||
*/
|
||||
@GET
|
||||
@Path("{id}/browse")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, the browse feature is not supported by this type of repositories."),
|
||||
@ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(BrowserResult.class)
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
//J-
|
||||
@@ -581,15 +568,7 @@ public class RepositoryResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Repository} with the specified type and name.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>404 not found,
|
||||
* no repository with the specified type and name available</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns the {@link Repository} with the specified type and name.
|
||||
*
|
||||
* @param type the type of the repository
|
||||
* @param name the name of the repository
|
||||
@@ -598,8 +577,13 @@ public class RepositoryResource
|
||||
*/
|
||||
@GET
|
||||
@Path("{type: [a-z]+}/{name: .*}")
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 404, condition = "not found, no repository with the specified type and name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(Repository.class)
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Response getByTypeAndName(@PathParam("type") String type,
|
||||
@PathParam("name") String name)
|
||||
{
|
||||
@@ -621,17 +605,7 @@ public class RepositoryResource
|
||||
|
||||
/**
|
||||
* Returns the {@link Changeset} from the given repository
|
||||
* with the specified revision.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>400 bad request, the changeset feature is not
|
||||
* supported by this type of repositories.</li>
|
||||
* <li>404 not found, if the repository or
|
||||
* the revision could not be found</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* with the specified revision.
|
||||
*
|
||||
* @param id the id of the repository
|
||||
* @param revision the revision of the changeset
|
||||
@@ -643,6 +617,14 @@ public class RepositoryResource
|
||||
*/
|
||||
@GET
|
||||
@Path("{id}/changeset/{revision}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, the changeset feature is not supported by this type of repositories."),
|
||||
@ResponseCode(code = 404, condition = "not found, the repository or the revision could not be found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(Changeset.class)
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Response getChangeset(@PathParam("id") String id,
|
||||
@PathParam("revision") String revision)
|
||||
throws IOException, RepositoryException
|
||||
@@ -691,16 +673,7 @@ public class RepositoryResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of {@link Changeset} for the given repository.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>400 bad request, the changeset feature is not
|
||||
* supported by this type of repositories.</li>
|
||||
* <li>404 not found, if the repository or the path could not be found</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns a list of {@link Changeset} for the given repository.
|
||||
*
|
||||
* @param id the id of the repository
|
||||
* @param path path of a file
|
||||
@@ -716,6 +689,12 @@ public class RepositoryResource
|
||||
*/
|
||||
@GET
|
||||
@Path("{id}/changesets")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, the changeset feature is not supported by this type of repositories."),
|
||||
@ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(ChangesetPagingResult.class)
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
//J-
|
||||
@@ -784,16 +763,7 @@ public class RepositoryResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of a file.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>400 bad request, the content feature is not
|
||||
* supported by this type of repositories.</li>
|
||||
* <li>404 not found, if the repository or the path could not be found</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns the content of a file.
|
||||
*
|
||||
* @param id the id of the repository
|
||||
* @param revision the revision of the file
|
||||
@@ -803,6 +773,12 @@ public class RepositoryResource
|
||||
*/
|
||||
@GET
|
||||
@Path("{id}/content")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, the content feature is not supported by this type of repositories."),
|
||||
@ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(StreamingOutput.class)
|
||||
@Produces({ MediaType.APPLICATION_OCTET_STREAM })
|
||||
public Response getContent(@PathParam("id") String id,
|
||||
@@ -855,16 +831,7 @@ public class RepositoryResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modifications of a {@link Changeset}.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>400 bad request, the content feature is not
|
||||
* supported by this type of repositories.</li>
|
||||
* <li>404 not found, if the repository or the path could not be found</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns the modifications of a {@link Changeset}.
|
||||
*
|
||||
* @param id the id of the repository
|
||||
* @param revision the revision of the file
|
||||
@@ -878,6 +845,12 @@ public class RepositoryResource
|
||||
*/
|
||||
@GET
|
||||
@Path("{id}/diff")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, the diff feature is not supported by this type of repositories."),
|
||||
@ResponseCode(code = 404, condition = "not found, the repository or the path could not be found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(DiffStreamingOutput.class)
|
||||
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||
public Response getDiff(@PathParam("id") String id,
|
||||
@@ -943,16 +916,7 @@ public class RepositoryResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link Tags} of a repository.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>400 bad request, the content feature is not
|
||||
* supported by this type of repositories.</li>
|
||||
* <li>404 not found, if the repository or the path could not be found</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns all {@link Tags} of a repository.
|
||||
*
|
||||
* @param id the id of the repository
|
||||
*
|
||||
@@ -965,6 +929,14 @@ public class RepositoryResource
|
||||
*/
|
||||
@GET
|
||||
@Path("{id}/tags")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, the tag feature is not supported by this type of repositories."),
|
||||
@ResponseCode(code = 404, condition = "not found, the repository could not be found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(Tags.class)
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public Response getTags(@PathParam("id") String id)
|
||||
throws RepositoryException, IOException
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -40,8 +40,8 @@ import com.github.legman.Subscribe;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
|
||||
import sonia.scm.cache.Cache;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
@@ -64,12 +64,13 @@ import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
/**
|
||||
*
|
||||
* RESTful Web Service Resource to search users and groups. This endpoint can be used to implement typeahead input
|
||||
* fields for permissions.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@Singleton
|
||||
@Path("search")
|
||||
@ExternallyManagedLifecycle
|
||||
public class SearchResource
|
||||
{
|
||||
|
||||
@@ -140,12 +141,7 @@ public class SearchResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of groups found by the given search string.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns a list of groups found by the given search string.
|
||||
*
|
||||
* @param queryString the search string
|
||||
*
|
||||
@@ -153,6 +149,10 @@ public class SearchResource
|
||||
*/
|
||||
@GET
|
||||
@Path("groups")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public SearchResults searchGroups(@QueryParam("query") String queryString)
|
||||
{
|
||||
@@ -176,12 +176,7 @@ public class SearchResource
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of users found by the given search string.<br />
|
||||
* <br />
|
||||
* <ul>
|
||||
* <li>200 success</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns a list of users found by the given search string.
|
||||
*
|
||||
* @param queryString the search string
|
||||
*
|
||||
@@ -189,6 +184,10 @@ public class SearchResource
|
||||
*/
|
||||
@GET
|
||||
@Path("users")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
public SearchResults searchUsers(@QueryParam("query") String queryString)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
|
||||
@@ -37,13 +37,14 @@ package sonia.scm.api.rest.resources;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authc.credential.PasswordService;
|
||||
|
||||
import org.codehaus.enunciate.jaxrs.TypeHint;
|
||||
import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle;
|
||||
|
||||
import sonia.scm.security.Role;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserException;
|
||||
@@ -73,12 +74,12 @@ import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
/**
|
||||
*
|
||||
* RESTful Web Service Resource to manage users.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@Singleton
|
||||
@Path("users")
|
||||
@ExternallyManagedLifecycle
|
||||
public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
{
|
||||
|
||||
@@ -107,15 +108,7 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a new user.<br />
|
||||
* This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>201 create success</li>
|
||||
* <li>403 forbidden, the current user has no admin privileges</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Creates a new user. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param uriInfo current uri informations
|
||||
* @param user the user to be created
|
||||
@@ -123,6 +116,14 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "create success", additionalHeaders = {
|
||||
@ResponseHeader(name = "Location", description = "uri to the created group")
|
||||
}),
|
||||
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
@Override
|
||||
public Response create(@Context UriInfo uriInfo, User user)
|
||||
@@ -131,15 +132,7 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a user.<br />
|
||||
* This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>201 delete success</li>
|
||||
* <li>403 forbidden, the current user has no admin privileges</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Deletes a user. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param name the name of the user to delete.
|
||||
*
|
||||
@@ -147,6 +140,12 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
*/
|
||||
@DELETE
|
||||
@Path("{id}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "delete success"),
|
||||
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Override
|
||||
public Response delete(@PathParam("id") String name)
|
||||
{
|
||||
@@ -154,15 +153,7 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the given user.<br />
|
||||
* This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>201 update successful</li>
|
||||
* <li>403 forbidden, the current user has no admin privileges</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Modifies the given user. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param uriInfo current uri informations
|
||||
* @param name name of the user to be modified
|
||||
@@ -172,6 +163,12 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
*/
|
||||
@PUT
|
||||
@Path("{id}")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "update success"),
|
||||
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
@Override
|
||||
public Response update(@Context UriInfo uriInfo,
|
||||
@@ -183,16 +180,7 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns a user.<br />
|
||||
* This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>403 forbidden, the current user has no admin privileges</li>
|
||||
* <li>404 not found, no user with the specified id/name available</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns a user. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param request the current request
|
||||
* @param id the id/name of the user
|
||||
@@ -202,6 +190,12 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@TypeHint(User.class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
|
||||
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
@Override
|
||||
public Response get(@Context Request request, @PathParam("id") String id)
|
||||
@@ -221,15 +215,7 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all users.<br />
|
||||
* This method requires admin privileges.<br />
|
||||
* <br />
|
||||
* Status codes:
|
||||
* <ul>
|
||||
* <li>200 get successful</li>
|
||||
* <li>403 forbidden, the current user has no admin privileges</li>
|
||||
* <li>500 internal server error</li>
|
||||
* </ul>
|
||||
* Returns all users. <strong>Note:</strong> This method requires admin privileges.
|
||||
*
|
||||
* @param request the current request
|
||||
* @param start the start value for paging
|
||||
@@ -241,6 +227,11 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
*/
|
||||
@GET
|
||||
@TypeHint(User[].class)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 403, condition = "forbidden, the current user has no admin privileges"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
|
||||
@Override
|
||||
public Response getAll(@Context Request request, @DefaultValue("0")
|
||||
@@ -254,14 +245,6 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param items
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected GenericEntity<Collection<User>> createGenericEntity(
|
||||
Collection<User> items)
|
||||
@@ -270,24 +253,12 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param user
|
||||
*/
|
||||
@Override
|
||||
protected void preCreate(User user)
|
||||
{
|
||||
encryptPassword(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param user
|
||||
*/
|
||||
|
||||
@Override
|
||||
protected void preUpate(User user)
|
||||
{
|
||||
@@ -304,14 +275,6 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param users
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected Collection<User> prepareForReturn(Collection<User> users)
|
||||
{
|
||||
@@ -326,14 +289,6 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param user
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected User prepareForReturn(User user)
|
||||
{
|
||||
@@ -342,42 +297,18 @@ public class UserResource extends AbstractManagerResource<User, UserException>
|
||||
return user;
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param user
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected String getId(User user)
|
||||
{
|
||||
return user.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected String getPathPart()
|
||||
{
|
||||
return PATH_PART;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param user
|
||||
*/
|
||||
private void encryptPassword(User user)
|
||||
{
|
||||
String password = user.getPassword();
|
||||
|
||||
@@ -63,8 +63,7 @@ public class GuavaCache<K, V>
|
||||
/**
|
||||
* the logger for GuavaCache
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(GuavaCache.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(GuavaCache.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
@@ -89,8 +88,7 @@ public class GuavaCache<K, V>
|
||||
@SuppressWarnings("unchecked")
|
||||
public GuavaCache(GuavaCacheConfiguration configuration, String name)
|
||||
{
|
||||
this(GuavaCaches.create(configuration, name),
|
||||
configuration.getCopyStrategy(), name);
|
||||
this(GuavaCaches.create(configuration, name), configuration.getCopyStrategy(), name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +115,7 @@ public class GuavaCache<K, V>
|
||||
this.copyStrategy = CopyStrategy.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -104,15 +104,18 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager
|
||||
* @param keyGenerator
|
||||
* @param repositoryDAO
|
||||
* @param handlerSet
|
||||
* @param repositoryMatcher
|
||||
*/
|
||||
@Inject
|
||||
public DefaultRepositoryManager(ScmConfiguration configuration,
|
||||
SCMContextProvider contextProvider, KeyGenerator keyGenerator,
|
||||
RepositoryDAO repositoryDAO, Set<RepositoryHandler> handlerSet)
|
||||
RepositoryDAO repositoryDAO, Set<RepositoryHandler> handlerSet,
|
||||
RepositoryMatcher repositoryMatcher)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.keyGenerator = keyGenerator;
|
||||
this.repositoryDAO = repositoryDAO;
|
||||
this.repositoryMatcher = repositoryMatcher;
|
||||
|
||||
//J-
|
||||
ThreadFactory factory = new ThreadFactoryBuilder()
|
||||
@@ -558,7 +561,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager
|
||||
|
||||
for (Repository r : repositories)
|
||||
{
|
||||
if (type.equals(r.getType()) && isNameMatching(r, uri))
|
||||
if (repositoryMatcher.matches(r, type, uri))
|
||||
{
|
||||
check.check(r);
|
||||
repository = r.clone();
|
||||
@@ -715,30 +718,6 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager
|
||||
return handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param repository
|
||||
* @param path
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private boolean isNameMatching(Repository repository, String path)
|
||||
{
|
||||
boolean result = false;
|
||||
String name = repository.getName();
|
||||
|
||||
if (path.startsWith(name))
|
||||
{
|
||||
String sub = path.substring(name.length());
|
||||
|
||||
result = Util.isEmpty(sub) || sub.startsWith(HttpUtil.SEPARATOR_PATH);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
@@ -758,4 +737,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager
|
||||
|
||||
/** Field description */
|
||||
private final Set<Type> types;
|
||||
|
||||
/** Field description */
|
||||
private RepositoryMatcher repositoryMatcher;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.inject.Inject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
/**
|
||||
* RepositoryMatcher is able to check if a repository matches the requested path.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.54
|
||||
*/
|
||||
public final class RepositoryMatcher {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RepositoryMatcher.class);
|
||||
|
||||
private static final RepositoryPathMatcher DEFAULT_PATH_MATCHER = new DefaultRepositoryPathMatcher();
|
||||
|
||||
private final Map<String, RepositoryPathMatcher> pathMatchers;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param pathMatchers injected set of {@link RepositoryPathMatcher}.
|
||||
*/
|
||||
@Inject
|
||||
public RepositoryMatcher(Set<RepositoryPathMatcher> pathMatchers) {
|
||||
this.pathMatchers = Maps.newHashMap();
|
||||
for ( RepositoryPathMatcher pathMatcher : pathMatchers ) {
|
||||
LOG.info("register custom repository path matcher for type {}", pathMatcher.getType());
|
||||
this.pathMatchers.put(pathMatcher.getType(), pathMatcher);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} is the repository matches the type and the name matches the requested path.
|
||||
*
|
||||
* @param repository repository
|
||||
* @param type type of repository
|
||||
* @param path requested path without context and without type information
|
||||
*
|
||||
* @return {@code true} is the repository matches
|
||||
*/
|
||||
public boolean matches(Repository repository, String type, String path) {
|
||||
return type.equals(repository.getType()) && isPathMatching(repository, path);
|
||||
}
|
||||
|
||||
private boolean isPathMatching(Repository repository, String path) {
|
||||
return getPathMatcherForType(repository.getType()).isPathMatching(repository, path);
|
||||
}
|
||||
|
||||
private RepositoryPathMatcher getPathMatcherForType(String type) {
|
||||
RepositoryPathMatcher pathMatcher = pathMatchers.get(type);
|
||||
if (pathMatcher == null) {
|
||||
pathMatcher = DEFAULT_PATH_MATCHER;
|
||||
}
|
||||
return pathMatcher;
|
||||
}
|
||||
|
||||
private static class DefaultRepositoryPathMatcher implements RepositoryPathMatcher {
|
||||
|
||||
@Override
|
||||
public boolean isPathMatching(Repository repository, String path) {
|
||||
String name = repository.getName();
|
||||
|
||||
if (path.startsWith(name)) {
|
||||
String sub = path.substring(name.length());
|
||||
|
||||
return Util.isEmpty(sub) || sub.startsWith(HttpUtil.SEPARATOR_PATH);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return "any";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.EagerSingleton;
|
||||
import sonia.scm.ModificationHandlerEvent;
|
||||
import sonia.scm.event.HandlerEvent;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupEvent;
|
||||
import sonia.scm.group.GroupModificationEvent;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryEvent;
|
||||
import sonia.scm.repository.RepositoryModificationEvent;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserEvent;
|
||||
import sonia.scm.user.UserModificationEvent;
|
||||
|
||||
/**
|
||||
* Receives all kinds of events, which affects authorization relevant data and fires an
|
||||
* {@link AuthorizationChangedEvent} if authorization data has changed.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
@EagerSingleton
|
||||
public class AuthorizationChangedEventProducer {
|
||||
|
||||
/**
|
||||
* the logger for AuthorizationChangedEventProducer
|
||||
*/
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuthorizationChangedEventProducer.class);
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*/
|
||||
public AuthorizationChangedEventProducer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the cache of a user which was modified. The cache entries for the user will be invalidated for the
|
||||
* following reasons:
|
||||
* <ul>
|
||||
* <li>Admin or Active flag was modified.</li>
|
||||
* <li>New user created, for the case of old cache values</li>
|
||||
* <li>User deleted</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param event user event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(UserEvent event) {
|
||||
if (event.getEventType().isPost()) {
|
||||
if (isModificationEvent(event)) {
|
||||
handleUserModificationEvent((UserModificationEvent) event);
|
||||
} else {
|
||||
handleUserEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isModificationEvent(HandlerEvent<?> event) {
|
||||
return event instanceof ModificationHandlerEvent;
|
||||
}
|
||||
|
||||
private void handleUserEvent(UserEvent event) {
|
||||
String username = event.getItem().getName();
|
||||
logger.debug(
|
||||
"fire authorization changed event for user {}, because of user {} event", username, event.getEventType()
|
||||
);
|
||||
fireEventForUser(username);
|
||||
}
|
||||
|
||||
private void handleUserModificationEvent(UserModificationEvent event) {
|
||||
String username = event.getItem().getId();
|
||||
User beforeModification = event.getItemBeforeModification();
|
||||
if (isAuthorizationDataModified(event.getItem(), beforeModification)) {
|
||||
logger.debug(
|
||||
"fire authorization changed event for user {}, because of a authorization relevant field has changed",
|
||||
username
|
||||
);
|
||||
fireEventForUser(username);
|
||||
} else {
|
||||
logger.debug(
|
||||
"authorization changed event for user {} is not fired, because no authorization relevant field has changed",
|
||||
username
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAuthorizationDataModified(User user, User beforeModification) {
|
||||
return user.isAdmin() != beforeModification.isAdmin() || user.isActive() != beforeModification.isActive();
|
||||
}
|
||||
|
||||
private void fireEventForUser(String username) {
|
||||
sendEvent(AuthorizationChangedEvent.createForUser(username));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the whole cache, if a repository has changed. The cache get cleared for one of the following reasons:
|
||||
* <ul>
|
||||
* <li>New repository created</li>
|
||||
* <li>Repository was removed</li>
|
||||
* <li>Archived, Public readable or permission field of the repository was modified</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param event repository event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(RepositoryEvent event) {
|
||||
if (event.getEventType().isPost()) {
|
||||
if (isModificationEvent(event)) {
|
||||
handleRepositoryModificationEvent((RepositoryModificationEvent) event);
|
||||
} else {
|
||||
handleRepositoryEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRepositoryModificationEvent(RepositoryModificationEvent event) {
|
||||
Repository repository = event.getItem();
|
||||
if (isAuthorizationDataModified(repository, event.getItemBeforeModification())) {
|
||||
logger.debug(
|
||||
"fire authorization changed event, because a relevant field of repository {} has changed", repository.getName()
|
||||
);
|
||||
fireEventForEveryUser();
|
||||
} else {
|
||||
logger.debug(
|
||||
"authorization changed event is not fired, because non relevant field of repository {} has changed",
|
||||
repository.getName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAuthorizationDataModified(Repository repository, Repository beforeModification) {
|
||||
return repository.isArchived() != beforeModification.isArchived()
|
||||
|| repository.isPublicReadable() != beforeModification.isPublicReadable()
|
||||
|| ! repository.getPermissions().equals(beforeModification.getPermissions());
|
||||
}
|
||||
|
||||
private void fireEventForEveryUser() {
|
||||
sendEvent(AuthorizationChangedEvent.createForEveryUser());
|
||||
}
|
||||
|
||||
private void handleRepositoryEvent(RepositoryEvent event){
|
||||
logger.debug(
|
||||
"fire authorization changed event, because of received {} event for repository {}",
|
||||
event.getEventType(), event.getItem().getName()
|
||||
);
|
||||
fireEventForEveryUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the whole cache if a group permission has changed and invalidates the cached entries of a user, if a
|
||||
* user permission has changed.
|
||||
*
|
||||
* @param event permission event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(StoredAssignedPermissionEvent event) {
|
||||
if (event.getEventType().isPost()) {
|
||||
StoredAssignedPermission permission = event.getPermission();
|
||||
if (permission.isGroupPermission()) {
|
||||
handleGroupPermissionChange(permission);
|
||||
} else {
|
||||
handleUserPermissionChange(permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleGroupPermissionChange(StoredAssignedPermission permission) {
|
||||
logger.debug(
|
||||
"fire authorization changed event, because global group permission {} has changed",
|
||||
permission.getId()
|
||||
);
|
||||
fireEventForEveryUser();
|
||||
}
|
||||
|
||||
private void handleUserPermissionChange(StoredAssignedPermission permission) {
|
||||
logger.debug(
|
||||
"fire authorization changed event for user {}, because permission {} has changed",
|
||||
permission.getName(), permission.getId()
|
||||
);
|
||||
fireEventForUser(permission.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the whole cache, if a group has changed. The cache get cleared for one of the following reasons:
|
||||
* <ul>
|
||||
* <li>New group created</li>
|
||||
* <li>Group was removed</li>
|
||||
* <li>Group members was modified</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param event group event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(GroupEvent event) {
|
||||
if (event.getEventType().isPost()) {
|
||||
if (isModificationEvent(event)) {
|
||||
handleGroupModificationEvent((GroupModificationEvent) event);
|
||||
} else {
|
||||
handleGroupEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleGroupModificationEvent(GroupModificationEvent event) {
|
||||
Group group = event.getItem();
|
||||
if (isAuthorizationDataModified(group, event.getItemBeforeModification())) {
|
||||
logger.debug("fire authorization changed event, because group {} has changed", group.getId());
|
||||
fireEventForEveryUser();
|
||||
} else {
|
||||
logger.debug(
|
||||
"authorization changed event is not fired, because non relevant field of group {} has changed",
|
||||
group.getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAuthorizationDataModified(Group group, Group beforeModification) {
|
||||
return !group.getMembers().equals(beforeModification.getMembers());
|
||||
}
|
||||
|
||||
private void handleGroupEvent(GroupEvent event){
|
||||
logger.debug(
|
||||
"fire authorization changed event, because of received group event {} for group {}",
|
||||
event.getEventType(),
|
||||
event.getItem().getId()
|
||||
);
|
||||
fireEventForEveryUser();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected void sendEvent(AuthorizationChangedEvent event) {
|
||||
ScmEventBus.getInstance().post(event);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -55,24 +55,17 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import sonia.scm.cache.Cache;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.group.GroupEvent;
|
||||
import sonia.scm.group.GroupNames;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.repository.RepositoryEvent;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserEvent;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupModificationEvent;
|
||||
import sonia.scm.repository.RepositoryModificationEvent;
|
||||
import sonia.scm.user.UserModificationEvent;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -99,7 +92,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
LoggerFactory.getLogger(DefaultAuthorizationCollector.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
@@ -144,187 +137,14 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
return authorizationInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the cache of a user which was modified. The cache entries for the user will be invalidated for the
|
||||
* following reasons:
|
||||
* <ul>
|
||||
* <li>Admin or Active flag was modified.</li>
|
||||
* <li>New user created, for the case of old cache values</li>
|
||||
* <li>User deleted</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param event user event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(UserEvent event)
|
||||
{
|
||||
if (event.getEventType().isPost())
|
||||
{
|
||||
User user = event.getItem();
|
||||
String username = user.getId();
|
||||
if (event instanceof UserModificationEvent)
|
||||
{
|
||||
User beforeModification = ((UserModificationEvent) event).getItemBeforeModification();
|
||||
if (shouldCacheBeCleared(user, beforeModification))
|
||||
{
|
||||
logger.debug("invalidate cache of user {}, because of a permission relevant field has changed", username);
|
||||
invalidateUserCache(username);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug("cache of user {} is not invalidated, because no permission relevant field has changed", username);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug("invalidate cache of user {}, because of user {} event", username, event.getEventType());
|
||||
invalidateUserCache(username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldCacheBeCleared(User user, User beforeModification)
|
||||
{
|
||||
return user.isAdmin() != beforeModification.isAdmin() || user.isActive() != beforeModification.isActive();
|
||||
}
|
||||
|
||||
private void invalidateUserCache(final String username)
|
||||
{
|
||||
cache.removeAll((CacheKey item) -> username.equalsIgnoreCase(item.username));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the whole cache, if a repository has changed. The cache get cleared for one of the following reasons:
|
||||
* <ul>
|
||||
* <li>New repository created</li>
|
||||
* <li>Repository was removed</li>
|
||||
* <li>Archived, Public readable or permission field of the repository was modified</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param event repository event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(RepositoryEvent event)
|
||||
{
|
||||
if (event.getEventType().isPost())
|
||||
{
|
||||
Repository repository = event.getItem();
|
||||
|
||||
if (event instanceof RepositoryModificationEvent)
|
||||
{
|
||||
Repository beforeModification = ((RepositoryModificationEvent) event).getItemBeforeModification();
|
||||
if (shouldCacheBeCleared(repository, beforeModification))
|
||||
{
|
||||
logger.debug("clear cache, because a relevant field of repository {} has changed", repository.getName());
|
||||
cache.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug(
|
||||
"cache is not invalidated, because non relevant field of repository {} has changed",
|
||||
repository.getName()
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug("clear cache, received {} event of repository {}", event.getEventType(), repository.getName());
|
||||
cache.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldCacheBeCleared(Repository repository, Repository beforeModification)
|
||||
{
|
||||
return repository.isArchived() != beforeModification.isArchived()
|
||||
|| repository.isPublicReadable() != beforeModification.isPublicReadable()
|
||||
|| ! repository.getPermissions().equals(beforeModification.getPermissions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the whole cache if a group permission has changed and invalidates the cached entries of a user, if a
|
||||
* user permission has changed.
|
||||
*
|
||||
*
|
||||
* @param event permission event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(StoredAssignedPermissionEvent event)
|
||||
{
|
||||
if (event.getEventType().isPost())
|
||||
{
|
||||
StoredAssignedPermission permission = event.getPermission();
|
||||
if (permission.isGroupPermission())
|
||||
{
|
||||
logger.debug("clear cache, because global group permission {} has changed", permission.getId());
|
||||
cache.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug(
|
||||
"clear cache of user {}, because permission {} has changed",
|
||||
permission.getName(), event.getPermission().getId()
|
||||
);
|
||||
invalidateUserCache(permission.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the whole cache, if a group has changed. The cache get cleared for one of the following reasons:
|
||||
* <ul>
|
||||
* <li>New group created</li>
|
||||
* <li>Group was removed</li>
|
||||
* <li>Group members was modified</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param event group event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onEvent(GroupEvent event)
|
||||
{
|
||||
if (event.getEventType().isPost())
|
||||
{
|
||||
Group group = event.getItem();
|
||||
if (event instanceof GroupModificationEvent)
|
||||
{
|
||||
Group beforeModification = ((GroupModificationEvent) event).getItemBeforeModification();
|
||||
if (shouldCacheBeCleared(group, beforeModification))
|
||||
{
|
||||
logger.debug("clear cache, because group {} has changed", group.getId());
|
||||
cache.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug(
|
||||
"cache is not invalidated, because non relevant field of group {} has changed",
|
||||
group.getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug("clear cache, received group event {} for group {}", event.getEventType(), group.getId());
|
||||
cache.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldCacheBeCleared(Group group, Group beforeModification)
|
||||
{
|
||||
return !group.getMembers().equals(beforeModification.getMembers());
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param principals
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
AuthorizationInfo collect(PrincipalCollection principals)
|
||||
public AuthorizationInfo collect(PrincipalCollection principals)
|
||||
{
|
||||
Preconditions.checkNotNull(principals, "principals parameter is required");
|
||||
|
||||
@@ -456,6 +276,25 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
|| ((!perm.isGroupPermission()) && user.getName().equals(perm.getName()));
|
||||
//J+
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void invalidateCache(AuthorizationChangedEvent event) {
|
||||
if (event.isEveryUserAffected()) {
|
||||
invalidateUserCache(event.getNameOfAffectedUser());
|
||||
} else {
|
||||
invalidateCache();
|
||||
}
|
||||
}
|
||||
|
||||
private void invalidateUserCache(final String username) {
|
||||
logger.info("invalidate cache for user {}, because of a received authorization event", username);
|
||||
cache.removeAll((CacheKey item) -> username.equalsIgnoreCase(item.username));
|
||||
}
|
||||
|
||||
private void invalidateCache() {
|
||||
logger.info("invalidate cache, because of a received authorization event");
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
//~--- inner classes --------------------------------------------------------
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -158,8 +158,7 @@ public abstract class CacheManagerTestBase<C extends Cache>
|
||||
* @param c1
|
||||
* @param c2
|
||||
*/
|
||||
protected void assertIsSame(Cache<String, String> c1,
|
||||
Cache<String, String> c2)
|
||||
protected void assertIsSame(Cache<String, String> c1, Cache<String, String> c2)
|
||||
{
|
||||
assertSame(c1, c2);
|
||||
}
|
||||
|
||||
@@ -76,8 +76,6 @@ public class CreateRepositoriesITCase extends AbstractAdminITCaseBase
|
||||
*/
|
||||
public CreateRepositoriesITCase(String repositoryType)
|
||||
{
|
||||
System.out.append("==> CreateRepositoriesITCase - ").println(
|
||||
repositoryType);
|
||||
this.repositoryType = repositoryType;
|
||||
}
|
||||
|
||||
@@ -92,14 +90,14 @@ public class CreateRepositoriesITCase extends AbstractAdminITCaseBase
|
||||
@Parameters
|
||||
public static Collection<String[]> createParameters()
|
||||
{
|
||||
Collection<String[]> params = new ArrayList<String[]>();
|
||||
Collection<String[]> params = new ArrayList<>();
|
||||
|
||||
params.add(new String[] { "git" });
|
||||
params.add(new String[] { "git" });
|
||||
params.add(new String[] { "svn" });
|
||||
|
||||
if (IOUtil.search("hg") != null)
|
||||
{
|
||||
params.add(new String[] { "git" });
|
||||
params.add(new String[] { "hg" });
|
||||
}
|
||||
|
||||
return params;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
343
scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java
Normal file
343
scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java
Normal file
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.it;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.sun.jersey.api.client.Client;
|
||||
import com.sun.jersey.api.client.UniformInterfaceException;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import org.apache.shiro.crypto.hash.Sha256Hash;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.codehaus.jackson.xc.JaxbAnnotationIntrospector;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import static sonia.scm.it.IntegrationTestUtil.*;
|
||||
import static sonia.scm.it.RepositoryITUtil.*;
|
||||
import sonia.scm.repository.Permission;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserTestData;
|
||||
|
||||
/**
|
||||
* Integration tests for git lfs.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class GitLfsITCase {
|
||||
|
||||
@Rule
|
||||
public ExpectedException expectedException = ExpectedException.none();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
private Client adminClient;
|
||||
|
||||
private Repository repository;
|
||||
|
||||
public GitLfsITCase() {
|
||||
mapper.setAnnotationIntrospector(new JaxbAnnotationIntrospector());
|
||||
}
|
||||
|
||||
// lifecycle methods
|
||||
|
||||
@Before
|
||||
public void setUpTestDependencies() {
|
||||
adminClient = createAdminClient();
|
||||
repository = createRepository(adminClient, RepositoryTestData.createHeartOfGold("git"));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDownTestDependencies() {
|
||||
deleteRepository(adminClient, repository.getId());
|
||||
adminClient.destroy();
|
||||
}
|
||||
|
||||
// tests
|
||||
|
||||
@Test
|
||||
public void testLfsAPIWithAdminPermissions() throws IOException {
|
||||
uploadAndDownload(adminClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLfsAPIWithOwnerPermissions() throws IOException {
|
||||
uploadAndDownloadAsUser(PermissionType.OWNER);
|
||||
}
|
||||
|
||||
private void uploadAndDownloadAsUser(PermissionType permissionType) throws IOException {
|
||||
User trillian = UserTestData.createTrillian();
|
||||
trillian.setPassword("secret123");
|
||||
createUser(trillian);
|
||||
|
||||
try {
|
||||
repository.getPermissions().add(new Permission(trillian.getId(), permissionType));
|
||||
modifyRepository(repository);
|
||||
|
||||
Client client = createClient();
|
||||
authenticate(client, trillian.getId(), "secret123");
|
||||
|
||||
uploadAndDownload(client);
|
||||
} finally {
|
||||
removeUser(trillian);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLfsAPIWithWritePermissions() throws IOException {
|
||||
uploadAndDownloadAsUser(PermissionType.WRITE);
|
||||
}
|
||||
|
||||
private void createUser(User user) {
|
||||
adminClient.resource(REST_BASE_URL + "users.json").post(user);
|
||||
}
|
||||
|
||||
private void modifyRepository(Repository repository) {
|
||||
adminClient.resource(REST_BASE_URL + "repositories/" + repository.getId() + ".json").put(repository);
|
||||
}
|
||||
|
||||
private void removeUser(User user) {
|
||||
adminClient.resource(REST_BASE_URL + "users/" + user.getId() + ".json").delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLfsAPIWithoutWritePermissions() throws IOException {
|
||||
User trillian = UserTestData.createTrillian();
|
||||
trillian.setPassword("secret123");
|
||||
createUser(trillian);
|
||||
|
||||
expectedException.expect(UniformInterfaceException.class);
|
||||
expectedException.expectMessage(Matchers.containsString("403"));
|
||||
|
||||
|
||||
try {
|
||||
repository.getPermissions().add(new Permission(trillian.getId(), PermissionType.READ));
|
||||
modifyRepository(repository);
|
||||
|
||||
Client client = createClient();
|
||||
authenticate(client, trillian.getId(), "secret123");
|
||||
|
||||
uploadAndDownload(client);
|
||||
} finally {
|
||||
removeUser(trillian);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLfsDownloadWithReadPermissions() throws IOException {
|
||||
User trillian = UserTestData.createTrillian();
|
||||
trillian.setPassword("secret123");
|
||||
createUser(trillian);
|
||||
|
||||
|
||||
try {
|
||||
repository.getPermissions().add(new Permission(trillian.getId(), PermissionType.READ));
|
||||
modifyRepository(repository);
|
||||
|
||||
// upload data as admin
|
||||
String data = UUID.randomUUID().toString();
|
||||
byte[] dataAsBytes = data.getBytes(Charsets.UTF_8);
|
||||
LfsObject lfsObject = upload(adminClient, dataAsBytes);
|
||||
|
||||
Client client = createClient();
|
||||
authenticate(client, trillian.getId(), "secret123");
|
||||
|
||||
// download as user
|
||||
byte[] downloadedData = download(client, lfsObject);
|
||||
|
||||
// assert both are equal
|
||||
assertArrayEquals(dataAsBytes, downloadedData);
|
||||
} finally {
|
||||
removeUser(trillian);
|
||||
}
|
||||
}
|
||||
|
||||
// lfs api
|
||||
|
||||
private void uploadAndDownload(Client client) throws IOException {
|
||||
String data = UUID.randomUUID().toString();
|
||||
byte[] dataAsBytes = data.getBytes(Charsets.UTF_8);
|
||||
LfsObject lfsObject = upload(client, dataAsBytes);
|
||||
byte[] downloadedData = download(client, lfsObject);
|
||||
assertArrayEquals(dataAsBytes, downloadedData);
|
||||
}
|
||||
|
||||
private LfsObject upload(Client client, byte[] data) throws IOException {
|
||||
LfsObject lfsObject = createLfsObject(data);
|
||||
LfsRequestBody request = LfsRequestBody.createUploadRequest(lfsObject);
|
||||
LfsResponseBody response = request(client, request);
|
||||
|
||||
String uploadURL = response.objects[0].actions.upload.href;
|
||||
client.resource(uploadURL).put(data);
|
||||
|
||||
return lfsObject;
|
||||
}
|
||||
|
||||
private LfsResponseBody request(Client client, LfsRequestBody request) throws IOException {
|
||||
String batchUrl = createBatchUrl();
|
||||
String requestAsString = mapper.writeValueAsString(request);
|
||||
|
||||
return client
|
||||
.resource(batchUrl)
|
||||
.accept("application/vnd.git-lfs+json")
|
||||
.header("Content-Type", "application/vnd.git-lfs+json")
|
||||
.post(LfsResponseBody.class, requestAsString);
|
||||
}
|
||||
|
||||
private String createBatchUrl() {
|
||||
String url = repository.createUrl(BASE_URL);
|
||||
return url + "/info/lfs/objects/batch";
|
||||
}
|
||||
|
||||
private byte[] download(Client client, LfsObject lfsObject) throws IOException {
|
||||
LfsRequestBody request = LfsRequestBody.createDownloadRequest(lfsObject);
|
||||
LfsResponseBody response = request(client, request);
|
||||
|
||||
String downloadUrl = response.objects[0].actions.download.href;
|
||||
return client.resource(downloadUrl).get(byte[].class);
|
||||
}
|
||||
|
||||
private LfsObject createLfsObject(byte[] data) {
|
||||
Sha256Hash hash = new Sha256Hash(data);
|
||||
String oid = hash.toHex();
|
||||
return new LfsObject(oid, data.length);
|
||||
}
|
||||
|
||||
// LFS DTO objects
|
||||
|
||||
@XmlRootElement
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class LfsRequestBody {
|
||||
|
||||
private String operation;
|
||||
private String[] transfers = new String[]{ "basic" };
|
||||
private LfsObject[] objects;
|
||||
|
||||
public LfsRequestBody() {
|
||||
}
|
||||
|
||||
private LfsRequestBody(String operation, LfsObject[] objects) {
|
||||
this.operation = operation;
|
||||
this.objects = objects;
|
||||
}
|
||||
|
||||
public static LfsRequestBody createUploadRequest(LfsObject object) {
|
||||
return new LfsRequestBody("upload", new LfsObject[]{object});
|
||||
}
|
||||
|
||||
public static LfsRequestBody createDownloadRequest(LfsObject object) {
|
||||
return new LfsRequestBody("download", new LfsObject[]{object});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@XmlRootElement
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class LfsResponseBody {
|
||||
|
||||
private LfsObject[] objects;
|
||||
|
||||
public LfsResponseBody() {
|
||||
}
|
||||
|
||||
public LfsResponseBody(LfsObject[] objects) {
|
||||
this.objects = objects;
|
||||
}
|
||||
}
|
||||
|
||||
@XmlRootElement
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class LfsObject {
|
||||
|
||||
private String oid;
|
||||
private long size;
|
||||
private LfsActions actions;
|
||||
|
||||
public LfsObject() {
|
||||
}
|
||||
|
||||
public LfsObject(String oid, long size) {
|
||||
this.oid = oid;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public LfsObject(String oid, long size, LfsActions actions) {
|
||||
this.oid = oid;
|
||||
this.size = size;
|
||||
this.actions = actions;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@XmlRootElement
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class LfsActions {
|
||||
|
||||
private LfsAction upload;
|
||||
private LfsAction download;
|
||||
|
||||
public LfsActions() {
|
||||
}
|
||||
}
|
||||
|
||||
@XmlRootElement
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class LfsAction {
|
||||
|
||||
private String href;
|
||||
|
||||
public LfsAction() {
|
||||
}
|
||||
|
||||
public LfsAction(String href) {
|
||||
this.href = href;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.google.common.base.Stopwatch;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.inject.Provider;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.apache.shiro.authc.SimpleAuthenticationInfo;
|
||||
import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
|
||||
import org.apache.shiro.authz.AuthorizationInfo;
|
||||
import org.apache.shiro.mgt.DefaultSecurityManager;
|
||||
import org.apache.shiro.realm.AuthorizingRealm;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import static org.mockito.Mockito.*;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.Type;
|
||||
import sonia.scm.cache.GuavaCacheManager;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.security.AuthorizationCollector;
|
||||
import sonia.scm.security.DefaultKeyGenerator;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.security.SecuritySystem;
|
||||
import sonia.scm.user.UserTestData;
|
||||
|
||||
/**
|
||||
* Performance test for {@link RepositoryManager#getAll()}.
|
||||
*
|
||||
* @see <a href="https://goo.gl/PD1AeM">Issue 781</a>
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.52
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class DefaultRepositoryManagerPerfTest {
|
||||
|
||||
private static final int REPOSITORY_COUNT = 2000;
|
||||
|
||||
private static final String REPOSITORY_TYPE = "perf";
|
||||
|
||||
@Mock
|
||||
private SCMContextProvider contextProvider;
|
||||
|
||||
@Mock
|
||||
private RepositoryDAO repositoryDAO;
|
||||
|
||||
private final ScmConfiguration configuration = new ScmConfiguration();
|
||||
|
||||
private final KeyGenerator keyGenerator = new DefaultKeyGenerator();
|
||||
|
||||
@Mock
|
||||
private RepositoryHandler repositoryHandler;
|
||||
|
||||
private DefaultRepositoryManager repositoryManager;
|
||||
|
||||
@Mock
|
||||
private AuthorizationCollector authzCollector;
|
||||
|
||||
/**
|
||||
* Setup object under test.
|
||||
*/
|
||||
@Before
|
||||
public void setUpObjectUnderTest(){
|
||||
when(repositoryHandler.getType()).thenReturn(new Type(REPOSITORY_TYPE, REPOSITORY_TYPE));
|
||||
Set<RepositoryHandler> handlerSet = ImmutableSet.of(repositoryHandler);
|
||||
RepositoryMatcher repositoryMatcher = new RepositoryMatcher(Collections.<RepositoryPathMatcher>emptySet());
|
||||
|
||||
repositoryManager = new DefaultRepositoryManager(
|
||||
configuration,
|
||||
contextProvider,
|
||||
keyGenerator,
|
||||
repositoryDAO,
|
||||
handlerSet,
|
||||
repositoryMatcher
|
||||
);
|
||||
|
||||
setUpTestRepositories();
|
||||
|
||||
GuavaCacheManager cacheManager = new GuavaCacheManager();
|
||||
DefaultSecurityManager securityManager = new DefaultSecurityManager(new DummyRealm(authzCollector, cacheManager));
|
||||
|
||||
ThreadContext.bind(securityManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down test objects.
|
||||
*/
|
||||
@After
|
||||
public void tearDown(){
|
||||
ThreadContext.unbindSecurityManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start performance test and ensure that the timeout is not reached.
|
||||
*/
|
||||
@Test(timeout = 6000l)
|
||||
public void perfTestGetAll(){
|
||||
SecurityUtils.getSubject().login(new UsernamePasswordToken("trillian", "secret"));
|
||||
|
||||
List<Long> times = new ArrayList<>();
|
||||
for ( int i=0; i<3; i++ ) {
|
||||
times.add(benchGetAll());
|
||||
}
|
||||
|
||||
long average = calculateAverage(times);
|
||||
double value = (double) average / TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS);
|
||||
|
||||
// Too bad this functionality is not exposed as a regular method call
|
||||
System.out.println( String.format("%.4g s", value) );
|
||||
}
|
||||
|
||||
private long calculateAverage(List<Long> times) {
|
||||
Long sum = 0l;
|
||||
if(!times.isEmpty()) {
|
||||
for (Long time : times) {
|
||||
sum += time;
|
||||
}
|
||||
return Math.round(sum.doubleValue() / times.size());
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
private long benchGetAll(){
|
||||
Stopwatch sw = Stopwatch.createStarted();
|
||||
System.out.append("found ").append(String.valueOf(repositoryManager.getAll().size()));
|
||||
sw.stop();
|
||||
System.out.append(" in ").println(sw);
|
||||
return sw.elapsed(TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void setUpTestRepositories() {
|
||||
Map<String,Repository> repositories = new LinkedHashMap<>();
|
||||
for ( int i=0; i<REPOSITORY_COUNT; i++ ) {
|
||||
Repository repository = createTestRepository(i);
|
||||
repositories.put(repository.getId(), repository);
|
||||
}
|
||||
when(repositoryDAO.getAll()).thenReturn(repositories.values());
|
||||
}
|
||||
|
||||
private Repository createTestRepository(int number){
|
||||
Repository repository = new Repository(keyGenerator.createKey(), REPOSITORY_TYPE, "repo-" + number);
|
||||
repository.getPermissions().add(new Permission("trillian", PermissionType.READ));
|
||||
return repository;
|
||||
}
|
||||
|
||||
static class DummyRealm extends AuthorizingRealm {
|
||||
|
||||
private final AuthorizationCollector authzCollector;
|
||||
|
||||
public DummyRealm(AuthorizationCollector authzCollector, org.apache.shiro.cache.CacheManager cacheManager) {
|
||||
this.authzCollector = authzCollector;
|
||||
setCredentialsMatcher(new AllowAllCredentialsMatcher());
|
||||
setCacheManager(cacheManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
|
||||
SimplePrincipalCollection spc = new SimplePrincipalCollection(token.getPrincipal(), REPOSITORY_TYPE);
|
||||
spc.add(UserTestData.createTrillian(), REPOSITORY_TYPE);
|
||||
return new SimpleAuthenticationInfo(spc, REPOSITORY_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
|
||||
return authzCollector.collect();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class SetProvider implements Provider {
|
||||
|
||||
@Override
|
||||
public Object get() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -55,12 +55,12 @@ import static org.hamcrest.Matchers.*;
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.Stack;
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
@@ -492,19 +492,18 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository, Re
|
||||
@Test
|
||||
public void getRepositoryFromRequestUriTest() throws RepositoryException, IOException {
|
||||
RepositoryManager m = createManager();
|
||||
|
||||
m.init(contextProvider);
|
||||
|
||||
createRepository(m, new Repository("1", "hg", "scm"));
|
||||
createRepository(m, new Repository("2", "hg", "scm-test"));
|
||||
createRepository(m, new Repository("3", "git", "project1/test-1"));
|
||||
createRepository(m, new Repository("4", "git", "project1/test-2"));
|
||||
|
||||
assertEquals("scm", m.getFromUri("hg/scm").getName());
|
||||
assertEquals("scm-test", m.getFromUri("hg/scm-test").getName());
|
||||
assertEquals("scm-test", m.getFromUri("/hg/scm-test").getName());
|
||||
assertEquals("project1/test-1",
|
||||
m.getFromUri("/git/project1/test-1").getName());
|
||||
assertEquals("project1/test-1",
|
||||
m.getFromUri("/git/project1/test-1/ka/some/path").getName());
|
||||
assertEquals("project1/test-1", m.getFromUri("/git/project1/test-1").getName());
|
||||
assertEquals("project1/test-1", m.getFromUri("/git/project1/test-1/ka/some/path").getName());
|
||||
assertNull(m.getFromUri("/git/project1/test-3/ka/some/path"));
|
||||
}
|
||||
|
||||
@@ -543,7 +542,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository, Re
|
||||
configuration.setEnableRepositoryArchive(archiveEnabled);
|
||||
|
||||
return new DefaultRepositoryManager(configuration, contextProvider,
|
||||
keyGenerator, repositoryDAO, handlerSet);
|
||||
keyGenerator, repositoryDAO, handlerSet, createRepositoryMatcher());
|
||||
}
|
||||
|
||||
private void createRepository(RepositoryManager m, Repository repository)
|
||||
@@ -569,6 +568,10 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository, Re
|
||||
assertEquals(repo.getCreationDate(), other.getCreationDate());
|
||||
assertEquals(repo.getLastModified(), other.getLastModified());
|
||||
}
|
||||
|
||||
private RepositoryMatcher createRepositoryMatcher() {
|
||||
return new RepositoryMatcher(Collections.<RepositoryPathMatcher>emptySet());
|
||||
}
|
||||
|
||||
private Repository createRepository(Repository repository) throws RepositoryException, IOException {
|
||||
manager.create(repository);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import java.util.Set;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Before;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link RepositoryMatcher}.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.54
|
||||
*/
|
||||
public class RepositoryMatcherTest {
|
||||
|
||||
private RepositoryMatcher matcher;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Set<RepositoryPathMatcher> pathMatchers = Sets.<RepositoryPathMatcher>newHashSet(new AbcRepositoryPathMatcher());
|
||||
this.matcher = new RepositoryMatcher(pathMatchers);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatches() {
|
||||
assertFalse(matcher.matches(repository("hg", "scm"), "hg", "scm-test/ka"));
|
||||
assertFalse(matcher.matches(repository("git", "scm-test"), "hg", "scm-test"));
|
||||
|
||||
assertTrue(matcher.matches(repository("hg", "scm-test"), "hg", "scm-test/ka"));
|
||||
assertTrue(matcher.matches(repository("hg", "scm-test"), "hg", "scm-test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchesWithCustomPathMatcher() {
|
||||
assertFalse(matcher.matches(repository("abc", "scm"), "hg", "/long/path/with/abc"));
|
||||
assertTrue(matcher.matches(repository("abc", "scm"), "abc", "/long/path/with/abc"));
|
||||
}
|
||||
|
||||
private Repository repository(String type, String name) {
|
||||
return new Repository(type + "-" + name, type, name);
|
||||
}
|
||||
|
||||
private static class AbcRepositoryPathMatcher implements RepositoryPathMatcher {
|
||||
|
||||
@Override
|
||||
public boolean isPathMatching(Repository repository, String path) {
|
||||
return path.endsWith("abc");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return "abc";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,22 +50,14 @@ import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Rule;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.cache.Cache;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupEvent;
|
||||
import sonia.scm.group.GroupModificationEvent;
|
||||
import sonia.scm.group.GroupNames;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.repository.RepositoryEvent;
|
||||
import sonia.scm.repository.RepositoryModificationEvent;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserEvent;
|
||||
import sonia.scm.user.UserModificationEvent;
|
||||
import sonia.scm.user.UserTestData;
|
||||
|
||||
/**
|
||||
@@ -82,7 +74,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
|
||||
@Mock
|
||||
private CacheManager cacheManager;
|
||||
|
||||
|
||||
@Mock
|
||||
private RepositoryDAO repositoryDAO;
|
||||
|
||||
@@ -104,160 +96,6 @@ public class DefaultAuthorizationCollectorTest {
|
||||
collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.user.UserEvent)}.
|
||||
*/
|
||||
@Test
|
||||
public void testOnUserEvent()
|
||||
{
|
||||
User user = UserTestData.createDent();
|
||||
collector.onEvent(new UserEvent(HandlerEventType.BEFORE_CREATE, user));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
collector.onEvent(new UserEvent(HandlerEventType.CREATE, user));
|
||||
verify(cache).removeAll(Mockito.any(Predicate.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.user.UserEvent)} with modified user.
|
||||
*/
|
||||
@Test
|
||||
public void testOnUserModificationEvent()
|
||||
{
|
||||
User user = UserTestData.createDent();
|
||||
User userModified = UserTestData.createDent();
|
||||
userModified.setDisplayName("Super Dent");
|
||||
|
||||
collector.onEvent(new UserModificationEvent(HandlerEventType.BEFORE_CREATE, userModified, user));
|
||||
verify(cache, never()).removeAll(Mockito.any(Predicate.class));
|
||||
|
||||
collector.onEvent(new UserModificationEvent(HandlerEventType.CREATE, userModified, user));
|
||||
verify(cache, never()).removeAll(Mockito.any(Predicate.class));
|
||||
|
||||
userModified.setAdmin(true);
|
||||
|
||||
collector.onEvent(new UserModificationEvent(HandlerEventType.BEFORE_CREATE, userModified, user));
|
||||
verify(cache, never()).removeAll(Mockito.any(Predicate.class));
|
||||
|
||||
collector.onEvent(new UserModificationEvent(HandlerEventType.CREATE, userModified, user));
|
||||
verify(cache).removeAll(Mockito.any(Predicate.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.group.GroupEvent)}.
|
||||
*/
|
||||
@Test
|
||||
public void testOnGroupEvent()
|
||||
{
|
||||
Group group = new Group("xml", "base");
|
||||
collector.onEvent(new GroupEvent(HandlerEventType.BEFORE_CREATE, group));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
collector.onEvent(new GroupEvent(HandlerEventType.CREATE, group));
|
||||
verify(cache).clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.group.GroupEvent)} with modified groups.
|
||||
*/
|
||||
@Test
|
||||
public void testOnGroupModificationEvent()
|
||||
{
|
||||
Group group = new Group("xml", "base");
|
||||
Group modifiedGroup = new Group("xml", "base");
|
||||
collector.onEvent(new GroupModificationEvent(HandlerEventType.BEFORE_MODIFY, modifiedGroup, group));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
collector.onEvent(new GroupModificationEvent(HandlerEventType.MODIFY, modifiedGroup, group));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
modifiedGroup.add("test");
|
||||
collector.onEvent(new GroupModificationEvent(HandlerEventType.MODIFY, modifiedGroup, group));
|
||||
verify(cache).clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.repository.RepositoryEvent)}.
|
||||
*/
|
||||
@Test
|
||||
public void testOnRepositoryEvent()
|
||||
{
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
collector.onEvent(new RepositoryEvent(HandlerEventType.BEFORE_CREATE, repository));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
collector.onEvent(new RepositoryEvent(HandlerEventType.CREATE, repository));
|
||||
verify(cache).clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.repository.RepositoryEvent)} with modified repository.
|
||||
*/
|
||||
@Test
|
||||
public void testOnRepositoryModificationEvent()
|
||||
{
|
||||
Repository repositoryModified = RepositoryTestData.createHeartOfGold();
|
||||
repositoryModified.setName("test123");
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
|
||||
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
repository.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
|
||||
|
||||
collector.onEvent(new RepositoryModificationEvent(HandlerEventType.BEFORE_CREATE, repositoryModified, repository));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
|
||||
collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test123")));
|
||||
collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
|
||||
verify(cache).clear();
|
||||
|
||||
repositoryModified.setPermissions(
|
||||
Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.READ, true))
|
||||
);
|
||||
collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
|
||||
verify(cache, times(2)).clear();
|
||||
|
||||
repositoryModified.setPermissions(
|
||||
Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.WRITE))
|
||||
);
|
||||
collector.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
|
||||
verify(cache, times(3)).clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#onEvent(sonia.scm.security.StoredAssignedPermissionEvent)}.
|
||||
*/
|
||||
@Test
|
||||
public void testOnStoredAssignedPermissionEvent()
|
||||
{
|
||||
StoredAssignedPermission groupPermission = new StoredAssignedPermission(
|
||||
"123", new AssignedPermission("_authenticated", true, "repository:read:*")
|
||||
);
|
||||
collector.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, groupPermission));
|
||||
verify(cache, never()).clear();
|
||||
|
||||
collector.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.CREATE, groupPermission));
|
||||
verify(cache).clear();
|
||||
|
||||
|
||||
StoredAssignedPermission userPermission = new StoredAssignedPermission(
|
||||
"123", new AssignedPermission("trillian", false, "repository:read:*")
|
||||
);
|
||||
collector.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, userPermission));
|
||||
verify(cache, never()).removeAll(Mockito.any(Predicate.class));
|
||||
verify(cache).clear();
|
||||
|
||||
collector.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.CREATE, userPermission));
|
||||
verify(cache).removeAll(Mockito.any(Predicate.class));
|
||||
verify(cache).clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#collect()} without user role.
|
||||
*/
|
||||
@@ -386,8 +224,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two"));
|
||||
}
|
||||
|
||||
private void authenticate(User user, String group, String... groups)
|
||||
{
|
||||
private void authenticate(User user, String group, String... groups) {
|
||||
SimplePrincipalCollection spc = new SimplePrincipalCollection();
|
||||
spc.add(user.getName(), "unit");
|
||||
spc.add(user, "unit");
|
||||
@@ -396,4 +233,16 @@ public class DefaultAuthorizationCollectorTest {
|
||||
shiro.setSubject(subject);
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#invalidateCache(sonia.scm.security.AuthorizationChangedEvent)}.
|
||||
*/
|
||||
@Test
|
||||
public void testInvalidateCache() {
|
||||
collector.invalidateCache(AuthorizationChangedEvent.createForEveryUser());
|
||||
verify(cache).clear();
|
||||
|
||||
collector.invalidateCache(AuthorizationChangedEvent.createForUser("dent"));
|
||||
verify(cache).removeAll(Mockito.any(Predicate.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user