Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2018-10-02 09:55:08 +02:00
33 changed files with 758 additions and 57 deletions

View File

@@ -188,15 +188,14 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- TODO replace by proper version from maven central (group: com.github.sdorra) once its there. -->
<dependency> <dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId> <groupId>com.github.sdorra</groupId>
<artifactId>ssp-lib</artifactId> <artifactId>ssp-lib</artifactId>
<version>${ssp.version}</version> <version>${ssp.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId> <groupId>com.github.sdorra</groupId>
<artifactId>ssp-processor</artifactId> <artifactId>ssp-processor</artifactId>
<version>${ssp.version}</version> <version>${ssp.version}</version>
<optional>true</optional> <optional>true</optional>
@@ -765,7 +764,7 @@
<jetty.maven.version>9.2.10.v20150310</jetty.maven.version> <jetty.maven.version>9.2.10.v20150310</jetty.maven.version>
<!-- security libraries --> <!-- security libraries -->
<ssp.version>967c8fd521</ssp.version> <ssp.version>1.1.0</ssp.version>
<shiro.version>1.4.0</shiro.version> <shiro.version>1.4.0</shiro.version>
<!-- repostitory libraries --> <!-- repostitory libraries -->

View File

@@ -94,6 +94,12 @@
<artifactId>javax.ws.rs-api</artifactId> <artifactId>javax.ws.rs-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<scope>test</scope>
</dependency>
<!-- json --> <!-- json -->
<dependency> <dependency>
@@ -160,14 +166,13 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- TODO replace by proper version from maven central (group: com.github.sdorra) once its there. -->
<dependency> <dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId> <groupId>com.github.sdorra</groupId>
<artifactId>ssp-lib</artifactId> <artifactId>ssp-lib</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId> <groupId>com.github.sdorra</groupId>
<artifactId>ssp-processor</artifactId> <artifactId>ssp-processor</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>

View File

@@ -22,7 +22,7 @@ import com.github.sdorra.ssp.StaticPermissions;
@StaticPermissions( @StaticPermissions(
value = "configuration", value = "configuration",
permissions = {"read", "write"}, permissions = {"read", "write"},
globalPermissions = {} globalPermissions = {"list"}
) )
public interface Configuration extends PermissionObject { public interface Configuration extends PermissionObject {
} }

View File

@@ -60,7 +60,7 @@ import java.util.List;
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@StaticPermissions("group") @StaticPermissions(value = "group", globalPermissions = {"create", "list"})
@XmlRootElement(name = "groups") @XmlRootElement(name = "groups")
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class Group extends BasicPropertiesAware public class Group extends BasicPropertiesAware

View File

@@ -55,11 +55,10 @@ import java.security.Principal;
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@StaticPermissions("user") @StaticPermissions(value = "user", globalPermissions = {"create", "list"})
@XmlRootElement(name = "users") @XmlRootElement(name = "users")
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
{ {
/** Field description */ /** Field description */

View 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);
}
}

View File

@@ -15,6 +15,7 @@ public class VndMediaType {
public static final String PLAIN_TEXT_PREFIX = "text/" + SUBTYPE_PREFIX; public static final String PLAIN_TEXT_PREFIX = "text/" + SUBTYPE_PREFIX;
public static final String PLAIN_TEXT_SUFFIX = "+plain;v=" + VERSION; 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 USER = PREFIX + "user" + SUFFIX;
public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX;
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;

View File

@@ -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);
}
}
}

View 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/.+")
);
}
}

View File

@@ -62,18 +62,4 @@ public class MeITCase {
.assertType(s -> assertThat(s).isEqualTo(type)) .assertType(s -> assertThat(s).isEqualTo(type))
.assertPasswordLinkDoesNotExists(); .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);
}
} }

View File

@@ -93,21 +93,4 @@ public class UserITCase {
.assertType(s -> assertThat(s).isEqualTo(type)) .assertType(s -> assertThat(s).isEqualTo(type))
.assertPasswordLinkDoesNotExists(); .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);
}
} }

View File

@@ -24,6 +24,6 @@ public class RegExMatcher extends BaseMatcher<String> {
@Override @Override
public boolean matches(Object o) { public boolean matches(Object o) {
return Pattern.compile(pattern).matcher(o.toString()).matches(); return o != null && Pattern.compile(pattern).matcher(o.toString()).matches();
} }
} }

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}

View File

@@ -2,8 +2,10 @@
readOnly = secret, reader readOnly = secret, reader
writeOnly = secret, writer writeOnly = secret, writer
readWrite = secret, readerWriter readWrite = secret, readerWriter
admin = secret, admin
[roles] [roles]
reader = configuration:read:git reader = configuration:read:git
writer = configuration:write:git writer = configuration:write:git
readerWriter = configuration:*:git readerWriter = configuration:*:git
admin = *

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}

View File

@@ -2,8 +2,10 @@
readOnly = secret, reader readOnly = secret, reader
writeOnly = secret, writer writeOnly = secret, writer
readWrite = secret, readerWriter readWrite = secret, readerWriter
admin = secret, admin
[roles] [roles]
reader = configuration:read:hg reader = configuration:read:hg
writer = configuration:write:hg writer = configuration:write:hg
readerWriter = configuration:*:hg readerWriter = configuration:*:hg
admin = *

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}

View File

@@ -2,8 +2,10 @@
readOnly = secret, reader readOnly = secret, reader
writeOnly = secret, writer writeOnly = secret, writer
readWrite = secret, readerWriter readWrite = secret, readerWriter
admin = secret, admin
[roles] [roles]
reader = configuration:read:svn reader = configuration:read:svn
writer = configuration:write:svn writer = configuration:write:svn
readerWriter = configuration:*:svn readerWriter = configuration:*:svn
admin = *

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -3,7 +3,6 @@ package sonia.scm.api.v2.resources;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.core.UriInfo;
import java.net.URI; import java.net.URI;
class ResourceLinks { class ResourceLinks {
@@ -441,7 +440,6 @@ class ResourceLinks {
} }
} }
public UIPluginLinks uiPlugin() { public UIPluginLinks uiPlugin() {
return new UIPluginLinks(scmPathInfoStore.get()); return new UIPluginLinks(scmPathInfoStore.get());
} }
@@ -473,4 +471,45 @@ class ResourceLinks {
return uiPluginCollectionLinkBuilder.method("plugins").parameters().method("getInstalledPlugins").parameters().href(); 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();
}
}
} }

View File

@@ -84,7 +84,7 @@ public class SecurityFilter extends HttpFilter
HttpServletResponse response, FilterChain chain) HttpServletResponse response, FilterChain chain)
throws IOException, ServletException throws IOException, ServletException
{ {
if (!SecurityRequests.isAuthenticationRequest(request)) if (!SecurityRequests.isAuthenticationRequest(request) && !SecurityRequests.isIndexRequest(request))
{ {
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
if (hasPermission(subject)) if (hasPermission(subject))

View File

@@ -56,6 +56,7 @@ import sonia.scm.plugin.Extension;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO; import sonia.scm.repository.RepositoryDAO;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserPermissions;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import java.util.List; import java.util.List;
@@ -254,6 +255,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
collectGlobalPermissions(builder, user, groups); collectGlobalPermissions(builder, user, groups);
collectRepositoryPermissions(builder, user, groups); collectRepositoryPermissions(builder, user, groups);
builder.add(canReadOwnUser(user));
permissions = builder.build(); permissions = builder.build();
} }
@@ -262,6 +264,10 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
return info; return info;
} }
private String canReadOwnUser(User user) {
return UserPermissions.read(user.getName()).asShiroString();
}
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
private boolean isUserPermitted(User user, GroupNames groups, private boolean isUserPermitted(User user, GroupNames groups,

View File

@@ -11,6 +11,7 @@ import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH;
public final class SecurityRequests { 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_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() {} private SecurityRequests() {}
@@ -23,4 +24,13 @@ public final class SecurityRequests {
return URI_LOGIN_PATTERN.matcher(uri).matches(); 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();
}
} }

View File

@@ -99,7 +99,7 @@ public class ApiAuthenticationFilter extends AuthenticationFilter
throws IOException, ServletException throws IOException, ServletException
{ {
// skip filter on login resource // skip filter on login resource
if (SecurityRequests.isAuthenticationRequest(request)) if (SecurityRequests.isAuthenticationRequest(request) )
{ {
chain.doFilter(request, response); chain.doFilter(request, response);
} }

View File

@@ -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");
}
}

View File

@@ -34,6 +34,8 @@ public class ResourceLinksMock {
when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo)); when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo));
when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(uriInfo)); when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(uriInfo));
when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(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; return resourceLinks;
} }

View File

@@ -37,6 +37,7 @@ package sonia.scm.it;
import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.ClientResponse;
import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.HalRepresentation;
import org.junit.Assume;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; import org.junit.runners.Parameterized;
import sonia.scm.api.rest.ObjectMapperProvider; import sonia.scm.api.rest.ObjectMapperProvider;
@@ -158,6 +159,7 @@ public class UserPermissionITCase extends AbstractPermissionITCaseBase<User>
@Override @Override
protected void checkGetAllResponse(ClientResponse response) protected void checkGetAllResponse(ClientResponse response)
{ {
Assume.assumeTrue(credentials.getUsername() == null);
if (!credentials.isAnonymous()) if (!credentials.isAnonymous())
{ {
assertNotNull(response); assertNotNull(response);

View File

@@ -57,6 +57,7 @@ import sonia.scm.repository.RepositoryTestData;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserTestData; import sonia.scm.user.UserTestData;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
@@ -160,7 +161,8 @@ public class DefaultAuthorizationCollectorTest {
AuthorizationInfo authInfo = collector.collect(); AuthorizationInfo authInfo = collector.collect();
assertThat(authInfo.getRoles(), Matchers.contains(Role.USER)); 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()); assertThat(authInfo.getObjectPermissions(), nullValue());
} }
@@ -207,7 +209,7 @@ public class DefaultAuthorizationCollectorTest {
AuthorizationInfo authInfo = collector.collect(); AuthorizationInfo authInfo = collector.collect();
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
assertThat(authInfo.getObjectPermissions(), nullValue()); 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(); AuthorizationInfo authInfo = collector.collect();
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
assertThat(authInfo.getObjectPermissions(), nullValue()); 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) { private void authenticate(User user, String group, String... groups) {