mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-11 07:55:47 +01:00
Merge with 2.0.0-m3
This commit is contained in:
7
pom.xml
7
pom.xml
@@ -188,15 +188,14 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- TODO replace by proper version from maven central (group: com.github.sdorra) once its there. -->
|
||||
<dependency>
|
||||
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>ssp-lib</artifactId>
|
||||
<version>${ssp.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>ssp-processor</artifactId>
|
||||
<version>${ssp.version}</version>
|
||||
<optional>true</optional>
|
||||
@@ -765,7 +764,7 @@
|
||||
<jetty.maven.version>9.2.10.v20150310</jetty.maven.version>
|
||||
|
||||
<!-- security libraries -->
|
||||
<ssp.version>967c8fd521</ssp.version>
|
||||
<ssp.version>1.1.0</ssp.version>
|
||||
<shiro.version>1.4.0</shiro.version>
|
||||
|
||||
<!-- repostitory libraries -->
|
||||
|
||||
@@ -94,6 +94,12 @@
|
||||
<artifactId>javax.ws.rs-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxrs</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- json -->
|
||||
|
||||
<dependency>
|
||||
@@ -160,14 +166,13 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- TODO replace by proper version from maven central (group: com.github.sdorra) once its there. -->
|
||||
<dependency>
|
||||
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>ssp-lib</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>ssp-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
@@ -22,7 +22,7 @@ import com.github.sdorra.ssp.StaticPermissions;
|
||||
@StaticPermissions(
|
||||
value = "configuration",
|
||||
permissions = {"read", "write"},
|
||||
globalPermissions = {}
|
||||
globalPermissions = {"list"}
|
||||
)
|
||||
public interface Configuration extends PermissionObject {
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ import java.util.List;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@StaticPermissions("group")
|
||||
@StaticPermissions(value = "group", globalPermissions = {"create", "list"})
|
||||
@XmlRootElement(name = "groups")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class Group extends BasicPropertiesAware
|
||||
|
||||
@@ -55,11 +55,10 @@ import java.security.Principal;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@StaticPermissions("user")
|
||||
@StaticPermissions(value = "user", globalPermissions = {"create", "list"})
|
||||
@XmlRootElement(name = "users")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class
|
||||
User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
|
||||
public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
|
||||
{
|
||||
|
||||
/** Field description */
|
||||
|
||||
40
scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java
Normal file
40
scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package sonia.scm.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class JsonEnricherBase implements JsonEnricher {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
protected JsonEnricherBase(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
protected boolean resultHasMediaType(String mediaType, JsonEnricherContext context) {
|
||||
return mediaType.equals(context.getResponseMediaType().toString());
|
||||
}
|
||||
|
||||
protected JsonNode value(Object object) {
|
||||
return objectMapper.convertValue(object, JsonNode.class);
|
||||
}
|
||||
|
||||
protected ObjectNode createObject() {
|
||||
return objectMapper.createObjectNode();
|
||||
}
|
||||
|
||||
protected ObjectNode createObject(Map<String, Object> values) {
|
||||
ObjectNode object = createObject();
|
||||
|
||||
values.forEach((key, value) -> object.set(key, value(value)));
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
protected void addPropertyNode(JsonNode parent, String newKey, JsonNode child) {
|
||||
((ObjectNode) parent).set(newKey, child);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ public class VndMediaType {
|
||||
public static final String PLAIN_TEXT_PREFIX = "text/" + SUBTYPE_PREFIX;
|
||||
public static final String PLAIN_TEXT_SUFFIX = "+plain;v=" + VERSION;
|
||||
|
||||
public static final String INDEX = PREFIX + "index" + SUFFIX;
|
||||
public static final String USER = PREFIX + "user" + SUFFIX;
|
||||
public static final String GROUP = PREFIX + "group" + SUFFIX;
|
||||
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package sonia.scm.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class JsonEnricherBaseTest {
|
||||
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
private TestJsonEnricher enricher = new TestJsonEnricher(objectMapper);
|
||||
|
||||
@Test
|
||||
public void testResultHasMediaType() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, null);
|
||||
|
||||
assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_JSON, context)).isTrue();
|
||||
assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_XML, context)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAppendLink() {
|
||||
ObjectNode root = objectMapper.createObjectNode();
|
||||
ObjectNode links = objectMapper.createObjectNode();
|
||||
root.set("_links", links);
|
||||
JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, root);
|
||||
enricher.enrich(context);
|
||||
|
||||
assertThat(links.get("awesome").get("href").asText()).isEqualTo("/my/awesome/link");
|
||||
}
|
||||
|
||||
private static class TestJsonEnricher extends JsonEnricherBase {
|
||||
|
||||
public TestJsonEnricher(ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(JsonEnricherContext context) {
|
||||
JsonNode gitConfigRefNode = createObject(singletonMap("href", value("/my/awesome/link")));
|
||||
|
||||
addPropertyNode(context.getResponseEntity().get("_links"), "awesome", gitConfigRefNode);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
48
scm-it/src/test/java/sonia/scm/it/IndexITCase.java
Normal file
48
scm-it/src/test/java/sonia/scm/it/IndexITCase.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package sonia.scm.it;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.it.utils.RestUtil;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import static sonia.scm.it.utils.RegExMatcher.matchesPattern;
|
||||
import static sonia.scm.it.utils.RestUtil.given;
|
||||
|
||||
public class IndexITCase {
|
||||
|
||||
@Test
|
||||
public void shouldLinkEverythingForAdmin() {
|
||||
given(VndMediaType.INDEX)
|
||||
|
||||
.when()
|
||||
.get(RestUtil.createResourceUrl(""))
|
||||
|
||||
.then()
|
||||
.statusCode(HttpStatus.SC_OK)
|
||||
.body(
|
||||
"_links.repositories.href", matchesPattern(".+/repositories/"),
|
||||
"_links.users.href", matchesPattern(".+/users/"),
|
||||
"_links.groups.href", matchesPattern(".+/groups/"),
|
||||
"_links.config.href", matchesPattern(".+/config"),
|
||||
"_links.gitConfig.href", matchesPattern(".+/config/git"),
|
||||
"_links.hgConfig.href", matchesPattern(".+/config/hg"),
|
||||
"_links.svnConfig.href", matchesPattern(".+/config/svn")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateLoginLinksForAnonymousAccess() {
|
||||
RestAssured.given() // do not specify user credentials
|
||||
|
||||
.when()
|
||||
.get(RestUtil.createResourceUrl(""))
|
||||
|
||||
.then()
|
||||
.statusCode(HttpStatus.SC_OK)
|
||||
.body(
|
||||
"_links.login.href", matchesPattern(".+/auth/.+")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -62,18 +62,4 @@ public class MeITCase {
|
||||
.assertType(s -> assertThat(s).isEqualTo(type))
|
||||
.assertPasswordLinkDoesNotExists();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGet403IfUserIsNotAdmin() {
|
||||
String newUser = "user";
|
||||
String password = "pass";
|
||||
String type = "xml";
|
||||
TestData.createUser(newUser, password, false, type);
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getMeUrl())
|
||||
.usernameAndPassword(newUser, password)
|
||||
.getMeResource()
|
||||
.assertStatusCode(403);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,21 +93,4 @@ public class UserITCase {
|
||||
.assertType(s -> assertThat(s).isEqualTo(type))
|
||||
.assertPasswordLinkDoesNotExists();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGet403IfUserIsNotAdmin() {
|
||||
String newUser = "user";
|
||||
String password = "pass";
|
||||
String type = "xml";
|
||||
TestData.createUser(newUser, password, false, type);
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getMeUrl())
|
||||
.usernameAndPassword(newUser, password)
|
||||
.getUserResource()
|
||||
.assertStatusCode(403);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,6 @@ public class RegExMatcher extends BaseMatcher<String> {
|
||||
|
||||
@Override
|
||||
public boolean matches(Object o) {
|
||||
return Pattern.compile(pattern).matcher(o.toString()).matches();
|
||||
return o != null && Pattern.compile(pattern).matcher(o.toString()).matches();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.web.JsonEnricherBase;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static sonia.scm.web.VndMediaType.INDEX;
|
||||
|
||||
@Extension
|
||||
public class GitConfigInIndexResource extends JsonEnricherBase {
|
||||
|
||||
private final Provider<ScmPathInfoStore> scmPathInfoStore;
|
||||
|
||||
@Inject
|
||||
public GitConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
this.scmPathInfoStore = scmPathInfoStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(JsonEnricherContext context) {
|
||||
if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
|
||||
String gitConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), GitConfigResource.class)
|
||||
.method("get")
|
||||
.parameters()
|
||||
.href();
|
||||
|
||||
JsonNode gitConfigRefNode = createObject(singletonMap("href", value(gitConfigUrl)));
|
||||
|
||||
addPropertyNode(context.getResponseEntity().get("_links"), "gitConfig", gitConfigRefNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.net.URI;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
|
||||
public class GitConfigInIndexResourceTest {
|
||||
|
||||
@Rule
|
||||
public final ShiroRule shiroRule = new ShiroRule();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectNode root = objectMapper.createObjectNode();
|
||||
private final GitConfigInIndexResource gitConfigInIndexResource;
|
||||
|
||||
public GitConfigInIndexResourceTest() {
|
||||
root.put("_links", objectMapper.createObjectNode());
|
||||
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
|
||||
pathInfoStore.set(() -> URI.create("/"));
|
||||
gitConfigInIndexResource = new GitConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "admin", password = "secret")
|
||||
public void admin() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
gitConfigInIndexResource.enrich(context);
|
||||
|
||||
assertEquals("/v2/config/git", root.get("_links").get("gitConfig").get("href").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly", password = "secret")
|
||||
public void user() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
gitConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anonymous() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
gitConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
readOnly = secret, reader
|
||||
writeOnly = secret, writer
|
||||
readWrite = secret, readerWriter
|
||||
admin = secret, admin
|
||||
|
||||
[roles]
|
||||
reader = configuration:read:git
|
||||
writer = configuration:write:git
|
||||
readerWriter = configuration:*:git
|
||||
admin = *
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.web.JsonEnricherBase;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static sonia.scm.web.VndMediaType.INDEX;
|
||||
|
||||
@Extension
|
||||
public class HgConfigInIndexResource extends JsonEnricherBase {
|
||||
|
||||
private final Provider<ScmPathInfoStore> scmPathInfoStore;
|
||||
|
||||
@Inject
|
||||
public HgConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
this.scmPathInfoStore = scmPathInfoStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(JsonEnricherContext context) {
|
||||
if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
|
||||
String hgConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), HgConfigResource.class)
|
||||
.method("get")
|
||||
.parameters()
|
||||
.href();
|
||||
|
||||
JsonNode hgConfigRefNode = createObject(singletonMap("href", value(hgConfigUrl)));
|
||||
|
||||
addPropertyNode(context.getResponseEntity().get("_links"), "hgConfig", hgConfigRefNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.net.URI;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
|
||||
public class HgConfigInIndexResourceTest {
|
||||
|
||||
@Rule
|
||||
public final ShiroRule shiroRule = new ShiroRule();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectNode root = objectMapper.createObjectNode();
|
||||
private final HgConfigInIndexResource hgConfigInIndexResource;
|
||||
|
||||
public HgConfigInIndexResourceTest() {
|
||||
root.put("_links", objectMapper.createObjectNode());
|
||||
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
|
||||
pathInfoStore.set(() -> URI.create("/"));
|
||||
hgConfigInIndexResource = new HgConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "admin", password = "secret")
|
||||
public void admin() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
hgConfigInIndexResource.enrich(context);
|
||||
|
||||
assertEquals("/v2/config/hg", root.get("_links").get("hgConfig").get("href").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly", password = "secret")
|
||||
public void user() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
hgConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anonymous() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
hgConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
readOnly = secret, reader
|
||||
writeOnly = secret, writer
|
||||
readWrite = secret, readerWriter
|
||||
admin = secret, admin
|
||||
|
||||
[roles]
|
||||
reader = configuration:read:hg
|
||||
writer = configuration:write:hg
|
||||
readerWriter = configuration:*:hg
|
||||
admin = *
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.web.JsonEnricherBase;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static sonia.scm.web.VndMediaType.INDEX;
|
||||
|
||||
@Extension
|
||||
public class SvnConfigInIndexResource extends JsonEnricherBase {
|
||||
|
||||
private final Provider<ScmPathInfoStore> scmPathInfoStore;
|
||||
|
||||
@Inject
|
||||
public SvnConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
this.scmPathInfoStore = scmPathInfoStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(JsonEnricherContext context) {
|
||||
if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
|
||||
String svnConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), SvnConfigResource.class)
|
||||
.method("get")
|
||||
.parameters()
|
||||
.href();
|
||||
|
||||
JsonNode svnConfigRefNode = createObject(singletonMap("href", value(svnConfigUrl)));
|
||||
|
||||
addPropertyNode(context.getResponseEntity().get("_links"), "svnConfig", svnConfigRefNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.net.URI;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
|
||||
public class SvnConfigInIndexResourceTest {
|
||||
|
||||
@Rule
|
||||
public final ShiroRule shiroRule = new ShiroRule();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectNode root = objectMapper.createObjectNode();
|
||||
private final SvnConfigInIndexResource svnConfigInIndexResource;
|
||||
|
||||
public SvnConfigInIndexResourceTest() {
|
||||
root.put("_links", objectMapper.createObjectNode());
|
||||
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
|
||||
pathInfoStore.set(() -> URI.create("/"));
|
||||
svnConfigInIndexResource = new SvnConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "admin", password = "secret")
|
||||
public void admin() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
svnConfigInIndexResource.enrich(context);
|
||||
|
||||
assertEquals("/v2/config/svn", root.get("_links").get("svnConfig").get("href").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly", password = "secret")
|
||||
public void user() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
svnConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anonymous() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
svnConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
readOnly = secret, reader
|
||||
writeOnly = secret, writer
|
||||
readWrite = secret, readerWriter
|
||||
admin = secret, admin
|
||||
|
||||
[roles]
|
||||
reader = configuration:read:svn
|
||||
writer = configuration:write:svn
|
||||
readerWriter = configuration:*:svn
|
||||
admin = *
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class IndexDto extends HalRepresentation {
|
||||
|
||||
private final String version;
|
||||
|
||||
IndexDto(String version, Links links) {
|
||||
super(links);
|
||||
this.version = version;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Links;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.group.GroupPermissions;
|
||||
import sonia.scm.user.UserPermissions;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
|
||||
public class IndexDtoGenerator {
|
||||
|
||||
private final ResourceLinks resourceLinks;
|
||||
private final SCMContextProvider scmContextProvider;
|
||||
|
||||
@Inject
|
||||
public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider) {
|
||||
this.resourceLinks = resourceLinks;
|
||||
this.scmContextProvider = scmContextProvider;
|
||||
}
|
||||
|
||||
public IndexDto generate() {
|
||||
Links.Builder builder = Links.linkingTo();
|
||||
builder.self(resourceLinks.index().self());
|
||||
builder.single(link("uiPlugins", resourceLinks.uiPluginCollection().self()));
|
||||
if (SecurityUtils.getSubject().isAuthenticated()) {
|
||||
builder.single(
|
||||
link("me", resourceLinks.me().self()),
|
||||
link("logout", resourceLinks.authentication().logout())
|
||||
);
|
||||
if (UserPermissions.list().isPermitted()) {
|
||||
builder.single(link("users", resourceLinks.userCollection().self()));
|
||||
}
|
||||
if (GroupPermissions.list().isPermitted()) {
|
||||
builder.single(link("groups", resourceLinks.groupCollection().self()));
|
||||
}
|
||||
if (ConfigurationPermissions.list().isPermitted()) {
|
||||
builder.single(link("config", resourceLinks.config().self()));
|
||||
}
|
||||
builder.single(link("repositories", resourceLinks.repositoryCollection().self()));
|
||||
} else {
|
||||
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
||||
}
|
||||
|
||||
return new IndexDto(scmContextProvider.getVersion(), builder.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
|
||||
@Path(IndexResource.INDEX_PATH_V2)
|
||||
public class IndexResource {
|
||||
public static final String INDEX_PATH_V2 = "v2/";
|
||||
|
||||
private final IndexDtoGenerator indexDtoGenerator;
|
||||
|
||||
@Inject
|
||||
public IndexResource(IndexDtoGenerator indexDtoGenerator) {
|
||||
this.indexDtoGenerator = indexDtoGenerator;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.INDEX)
|
||||
@TypeHint(IndexDto.class)
|
||||
public IndexDto getIndex() {
|
||||
return indexDtoGenerator.generate();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package sonia.scm.api.v2.resources;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.net.URI;
|
||||
|
||||
class ResourceLinks {
|
||||
@@ -441,7 +440,6 @@ class ResourceLinks {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public UIPluginLinks uiPlugin() {
|
||||
return new UIPluginLinks(scmPathInfoStore.get());
|
||||
}
|
||||
@@ -473,4 +471,45 @@ class ResourceLinks {
|
||||
return uiPluginCollectionLinkBuilder.method("plugins").parameters().method("getInstalledPlugins").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
public AuthenticationLinks authentication() {
|
||||
return new AuthenticationLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class AuthenticationLinks {
|
||||
private final LinkBuilder loginLinkBuilder;
|
||||
|
||||
AuthenticationLinks(ScmPathInfo pathInfo) {
|
||||
this.loginLinkBuilder = new LinkBuilder(pathInfo, AuthenticationResource.class);
|
||||
}
|
||||
|
||||
String formLogin() {
|
||||
return loginLinkBuilder.method("authenticateViaForm").parameters().href();
|
||||
}
|
||||
|
||||
String jsonLogin() {
|
||||
return loginLinkBuilder.method("authenticateViaJSONBody").parameters().href();
|
||||
}
|
||||
|
||||
String logout() {
|
||||
return loginLinkBuilder.method("logout").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
public IndexLinks index() {
|
||||
return new IndexLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class IndexLinks {
|
||||
private final LinkBuilder indexLinkBuilder;
|
||||
|
||||
IndexLinks(ScmPathInfo pathInfo) {
|
||||
indexLinkBuilder = new LinkBuilder(pathInfo, IndexResource.class);
|
||||
}
|
||||
|
||||
String self() {
|
||||
return indexLinkBuilder.method("getIndex").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public class SecurityFilter extends HttpFilter
|
||||
HttpServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException
|
||||
{
|
||||
if (!SecurityRequests.isAuthenticationRequest(request))
|
||||
if (!SecurityRequests.isAuthenticationRequest(request) && !SecurityRequests.isIndexRequest(request))
|
||||
{
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
if (hasPermission(subject))
|
||||
|
||||
@@ -56,6 +56,7 @@ import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserPermissions;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
import java.util.List;
|
||||
@@ -254,6 +255,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
|
||||
collectGlobalPermissions(builder, user, groups);
|
||||
collectRepositoryPermissions(builder, user, groups);
|
||||
builder.add(canReadOwnUser(user));
|
||||
permissions = builder.build();
|
||||
}
|
||||
|
||||
@@ -262,6 +264,10 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
return info;
|
||||
}
|
||||
|
||||
private String canReadOwnUser(User user) {
|
||||
return UserPermissions.read(user.getName()).asShiroString();
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
private boolean isUserPermitted(User user, GroupNames groups,
|
||||
|
||||
@@ -11,6 +11,7 @@ import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH;
|
||||
public final class SecurityRequests {
|
||||
|
||||
private static final Pattern URI_LOGIN_PATTERN = Pattern.compile(REST_API_PATH + "(?:/v2)?/auth/access_token");
|
||||
private static final Pattern URI_INDEX_PATTERN = Pattern.compile(REST_API_PATH + "/v2/?");
|
||||
|
||||
private SecurityRequests() {}
|
||||
|
||||
@@ -23,4 +24,13 @@ public final class SecurityRequests {
|
||||
return URI_LOGIN_PATTERN.matcher(uri).matches();
|
||||
}
|
||||
|
||||
public static boolean isIndexRequest(HttpServletRequest request) {
|
||||
String uri = request.getRequestURI().substring(request.getContextPath().length());
|
||||
return isIndexRequest(uri);
|
||||
}
|
||||
|
||||
public static boolean isIndexRequest(String uri) {
|
||||
return URI_INDEX_PATTERN.matcher(uri).matches();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ public class ApiAuthenticationFilter extends AuthenticationFilter
|
||||
throws IOException, ServletException
|
||||
{
|
||||
// skip filter on login resource
|
||||
if (SecurityRequests.isAuthenticationRequest(request))
|
||||
if (SecurityRequests.isAuthenticationRequest(request) )
|
||||
{
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini")
|
||||
public class IndexResourceTest {
|
||||
|
||||
@Rule
|
||||
public final ShiroRule shiroRule = new ShiroRule();
|
||||
|
||||
private final SCMContextProvider scmContextProvider = mock(SCMContextProvider.class);
|
||||
private final IndexDtoGenerator indexDtoGenerator = new IndexDtoGenerator(ResourceLinksMock.createMock(URI.create("/")), scmContextProvider);
|
||||
private final IndexResource indexResource = new IndexResource(indexDtoGenerator);
|
||||
|
||||
@Test
|
||||
public void shouldRenderLoginUrlsForUnauthenticatedRequest() {
|
||||
IndexDto index = indexResource.getIndex();
|
||||
|
||||
Assertions.assertThat(index.getLinks().getLinkBy("login")).matches(Optional::isPresent);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRenderSelfLinkForUnauthenticatedRequest() {
|
||||
IndexDto index = indexResource.getIndex();
|
||||
|
||||
Assertions.assertThat(index.getLinks().getLinkBy("self")).matches(Optional::isPresent);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRenderUiPluginsLinkForUnauthenticatedRequest() {
|
||||
IndexDto index = indexResource.getIndex();
|
||||
|
||||
Assertions.assertThat(index.getLinks().getLinkBy("uiPlugins")).matches(Optional::isPresent);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void shouldRenderSelfLinkForAuthenticatedRequest() {
|
||||
IndexDto index = indexResource.getIndex();
|
||||
|
||||
Assertions.assertThat(index.getLinks().getLinkBy("self")).matches(Optional::isPresent);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void shouldRenderUiPluginsLinkForAuthenticatedRequest() {
|
||||
IndexDto index = indexResource.getIndex();
|
||||
|
||||
Assertions.assertThat(index.getLinks().getLinkBy("uiPlugins")).matches(Optional::isPresent);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void shouldRenderMeUrlForAuthenticatedRequest() {
|
||||
IndexDto index = indexResource.getIndex();
|
||||
|
||||
Assertions.assertThat(index.getLinks().getLinkBy("me")).matches(Optional::isPresent);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void shouldRenderLogoutUrlForAuthenticatedRequest() {
|
||||
IndexDto index = indexResource.getIndex();
|
||||
|
||||
Assertions.assertThat(index.getLinks().getLinkBy("logout")).matches(Optional::isPresent);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void shouldRenderRepositoriesForAuthenticatedRequest() {
|
||||
IndexDto index = indexResource.getIndex();
|
||||
|
||||
Assertions.assertThat(index.getLinks().getLinkBy("repositories")).matches(Optional::isPresent);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void shouldNotRenderAdminLinksIfNotAuthorized() {
|
||||
IndexDto index = indexResource.getIndex();
|
||||
|
||||
Assertions.assertThat(index.getLinks().getLinkBy("users")).matches(o -> !o.isPresent());
|
||||
Assertions.assertThat(index.getLinks().getLinkBy("groups")).matches(o -> !o.isPresent());
|
||||
Assertions.assertThat(index.getLinks().getLinkBy("config")).matches(o -> !o.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "dent", password = "secret")
|
||||
public void shouldRenderAdminLinksIfAuthorized() {
|
||||
IndexDto index = indexResource.getIndex();
|
||||
|
||||
Assertions.assertThat(index.getLinks().getLinkBy("users")).matches(Optional::isPresent);
|
||||
Assertions.assertThat(index.getLinks().getLinkBy("groups")).matches(Optional::isPresent);
|
||||
Assertions.assertThat(index.getLinks().getLinkBy("config")).matches(Optional::isPresent);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGenerateVersion() {
|
||||
when(scmContextProvider.getVersion()).thenReturn("v1");
|
||||
|
||||
IndexDto index = indexResource.getIndex();
|
||||
|
||||
Assertions.assertThat(index.getVersion()).isEqualTo("v1");
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,8 @@ public class ResourceLinksMock {
|
||||
when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo));
|
||||
when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(uriInfo));
|
||||
when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(uriInfo));
|
||||
when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(uriInfo));
|
||||
when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo));
|
||||
|
||||
return resourceLinks;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ package sonia.scm.it;
|
||||
|
||||
import com.sun.jersey.api.client.ClientResponse;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import org.junit.Assume;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import sonia.scm.api.rest.ObjectMapperProvider;
|
||||
@@ -158,6 +159,7 @@ public class UserPermissionITCase extends AbstractPermissionITCaseBase<User>
|
||||
@Override
|
||||
protected void checkGetAllResponse(ClientResponse response)
|
||||
{
|
||||
Assume.assumeTrue(credentials.getUsername() == null);
|
||||
if (!credentials.isAnonymous())
|
||||
{
|
||||
assertNotNull(response);
|
||||
|
||||
@@ -57,6 +57,7 @@ import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserTestData;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
@@ -160,7 +161,8 @@ public class DefaultAuthorizationCollectorTest {
|
||||
|
||||
AuthorizationInfo authInfo = collector.collect();
|
||||
assertThat(authInfo.getRoles(), Matchers.contains(Role.USER));
|
||||
assertThat(authInfo.getStringPermissions(), hasSize(0));
|
||||
assertThat(authInfo.getStringPermissions(), hasSize(1));
|
||||
assertThat(authInfo.getStringPermissions(), contains("user:read:trillian"));
|
||||
assertThat(authInfo.getObjectPermissions(), nullValue());
|
||||
}
|
||||
|
||||
@@ -207,7 +209,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
AuthorizationInfo authInfo = collector.collect();
|
||||
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
|
||||
assertThat(authInfo.getObjectPermissions(), nullValue());
|
||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read,pull:one", "repository:read,pull,push:two"));
|
||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,7 +230,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
AuthorizationInfo authInfo = collector.collect();
|
||||
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
|
||||
assertThat(authInfo.getObjectPermissions(), nullValue());
|
||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two"));
|
||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian"));
|
||||
}
|
||||
|
||||
private void authenticate(User user, String group, String... groups) {
|
||||
|
||||
Reference in New Issue
Block a user