Merged in feature/autocomplet_endpoint_v2 (pull request #87)

Feature/autocomplet endpoint v2
This commit is contained in:
René Pfeuffer
2018-10-09 15:08:33 +00:00
23 changed files with 442 additions and 14 deletions

View File

@@ -114,4 +114,5 @@ public interface GenericDAO<T>
* @return all items
*/
public Collection<T> getAll();
}

View File

@@ -47,6 +47,9 @@ public interface Manager<T extends ModelObject>
extends HandlerBase<T>, LastModifiedAware
{
int DEFAULT_LIMIT = 5;
/**
* Reloads a object from store and overwrites all changes.
*

View File

@@ -0,0 +1,15 @@
package sonia.scm;
/**
* This is a reduced form of a model object.
* It can be used as search result to avoid returning the whole object properties.
*
* @author Mohamed Karray
*/
public interface ReducedModelObject {
String getId();
String getDisplayName();
}

View File

@@ -42,6 +42,7 @@ import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import sonia.scm.BasicPropertiesAware;
import sonia.scm.ModelObject;
import sonia.scm.ReducedModelObject;
import sonia.scm.util.Util;
import sonia.scm.util.ValidationUtil;
@@ -55,16 +56,16 @@ import java.util.List;
/**
* Organizes users into a group for easier permissions management.
*
*
* TODO for 2.0: Use a set instead of a list for members
*
* @author Sebastian Sdorra
*/
@StaticPermissions(value = "group", globalPermissions = {"create", "list"})
@StaticPermissions(value = "group", globalPermissions = {"create", "list", "autocomplete"})
@XmlRootElement(name = "groups")
@XmlAccessorType(XmlAccessType.FIELD)
public class Group extends BasicPropertiesAware
implements ModelObject, PermissionObject
implements ModelObject, PermissionObject, ReducedModelObject
{
/** Field description */
@@ -309,6 +310,11 @@ public class Group extends BasicPropertiesAware
return name;
}
@Override
public String getDisplayName() {
return description;
}
/**
* Returns a timestamp of the last modified date of this group.
*

View File

@@ -61,4 +61,14 @@ public interface GroupManager
* @return all groups assigned to the given member
*/
public Collection<Group> getGroupsForMember(String member);
/**
* Returns a {@link java.util.Collection} of filtered objects
*
* @param filter the searched string
* @return filtered object from the store
*/
Collection<Group> autocomplete(String filter);
}

View File

@@ -109,6 +109,11 @@ public class GroupManagerDecorator
return decorated.getGroupsForMember(member);
}
@Override
public Collection<Group> autocomplete(String filter) {
return decorated.autocomplete(filter);
}
//~--- fields ---------------------------------------------------------------
/** Field description */

View File

@@ -64,7 +64,7 @@ import java.util.List;
)
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "repositories")
public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject {
public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{
private static final long serialVersionUID = 3486560714961909711L;

View File

@@ -70,6 +70,12 @@ public class SearchRequest
this.ignoreCase = ignoreCase;
}
public SearchRequest(String query, boolean ignoreCase, int maxResults) {
this.query = query;
this.ignoreCase = ignoreCase;
this.maxResults = maxResults;
}
//~--- get methods ----------------------------------------------------------
/**

View File

@@ -41,6 +41,7 @@ import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import sonia.scm.BasicPropertiesAware;
import sonia.scm.ModelObject;
import sonia.scm.ReducedModelObject;
import sonia.scm.util.Util;
import sonia.scm.util.ValidationUtil;
@@ -55,10 +56,10 @@ import java.security.Principal;
*
* @author Sebastian Sdorra
*/
@StaticPermissions(value = "user", globalPermissions = {"create", "list"})
@StaticPermissions(value = "user", globalPermissions = {"create", "list", "autocomplete"})
@XmlRootElement(name = "users")
@XmlAccessorType(XmlAccessType.FIELD)
public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject
{
/** Field description */

View File

@@ -39,6 +39,7 @@ import sonia.scm.Manager;
import sonia.scm.search.Searchable;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.function.Consumer;
import static sonia.scm.user.ChangePasswordNotAllowedException.WRONG_USER_TYPE;
@@ -90,5 +91,13 @@ public interface UserManager
return getDefaultType().equals(user.getType());
}
/**
* Returns a {@link java.util.Collection} of filtered objects
*
* @param filter the searched string
* @return filtered object from the store
*/
Collection<User> autocomplete(String filter);
}

View File

@@ -121,6 +121,11 @@ public class UserManagerDecorator extends ManagerDecorator<User>
return decorated.getDefaultType();
}
@Override
public Collection<User> autocomplete(String filter) {
return decorated.autocomplete(filter);
}
//~--- fields ---------------------------------------------------------------
/** Field description */

View File

@@ -18,6 +18,7 @@ public class VndMediaType {
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 AUTOCOMPLETE = PREFIX + "autocomplete" + SUFFIX;
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;
public static final String PERMISSION = PREFIX + "permission" + SUFFIX;
public static final String CHANGESET = PREFIX + "changeset" + SUFFIX;

View File

@@ -35,17 +35,20 @@ package sonia.scm.xml;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.ImmutableList;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.GenericDAO;
import sonia.scm.ModelObject;
import sonia.scm.group.xml.XmlGroupDAO;
//~--- JDK imports ------------------------------------------------------------
import sonia.scm.store.ConfigurationStore;
import sonia.scm.util.AssertUtil;
import java.util.Collection;
import sonia.scm.store.ConfigurationStore;
import java.util.stream.Collectors;
//~--- JDK imports ------------------------------------------------------------
/**
*

View File

@@ -0,0 +1,80 @@
package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import org.hibernate.validator.constraints.NotEmpty;
import sonia.scm.ReducedModelObject;
import sonia.scm.group.GroupManager;
import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.validation.constraints.Size;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import java.util.Collection;
import java.util.stream.Collectors;
@Path(AutoCompleteResource.PATH)
public class AutoCompleteResource {
public static final String PATH = "v2/autocomplete/";
public static final int MIN_SEARCHED_CHARS = 2;
public static final String PARAMETER_IS_REQUIRED = "The parameter is required.";
public static final String INVALID_PARAMETER_LENGTH = "Invalid parameter length.";
private ReducedObjectModelToDtoMapper mapper;
private UserManager userManager;
private GroupManager groupManager;
@Inject
public AutoCompleteResource(ReducedObjectModelToDtoMapper mapper, UserManager userManager, GroupManager groupManager) {
this.mapper = mapper;
this.userManager = userManager;
this.groupManager = groupManager;
}
@GET
@Path("user")
@Produces(VndMediaType.AUTOCOMPLETE)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "if the searched string contains less than 2 characters"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user:autocomplete\" privilege"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response searchUser(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("filter") String filter) {
return map(userManager.autocomplete(filter));
}
@GET
@Path("group")
@Produces(VndMediaType.AUTOCOMPLETE)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "if the searched string contains less than 2 characters"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group:autocomplete\" privilege"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response searchGroup(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("filter") String filter) {
return map(groupManager.autocomplete(filter));
}
private <T extends ReducedModelObject> Response map(Collection<T> autocomplete) {
return Response.ok(autocomplete
.stream()
.map(mapper::map)
.collect(Collectors.toList()))
.build();
}
}

View File

@@ -37,6 +37,8 @@ public class MapperModule extends AbstractModule {
bind(FileObjectToFileObjectDtoMapper.class).to(Mappers.getMapper(FileObjectToFileObjectDtoMapper.class).getClass());
bind(ModificationsToDtoMapper.class).to(Mappers.getMapper(ModificationsToDtoMapper.class).getClass());
bind(ReducedObjectModelToDtoMapper.class).to(Mappers.getMapper(ReducedObjectModelToDtoMapper.class).getClass());
// no mapstruct required
bind(UIPluginDtoMapper.class);
bind(UIPluginDtoCollectionMapper.class);

View File

@@ -0,0 +1,16 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class ReducedObjectModelDto extends HalRepresentation {
private String id;
private String displayName;
}

View File

@@ -0,0 +1,13 @@
package sonia.scm.api.v2.resources;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import sonia.scm.ReducedModelObject;
@Mapper
public abstract class ReducedObjectModelToDtoMapper {
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract ReducedObjectModelDto map(ReducedModelObject modelObject);
}

View File

@@ -242,6 +242,12 @@ public class DefaultGroupManager extends AbstractGroupManager
return group;
}
@Override
public Collection<Group> autocomplete(String filter) {
GroupPermissions.autocomplete().check();
return search(new SearchRequest(filter,true, DEFAULT_LIMIT));
}
/**
* Method description
*

View File

@@ -52,9 +52,11 @@ import org.slf4j.LoggerFactory;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.group.GroupNames;
import sonia.scm.group.GroupPermissions;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.user.User;
import sonia.scm.user.UserPermissions;
import sonia.scm.util.Util;
@@ -256,6 +258,8 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
collectGlobalPermissions(builder, user, groups);
collectRepositoryPermissions(builder, user, groups);
builder.add(canReadOwnUser(user));
builder.add(getUserAutocompletePermission());
builder.add(getGroupAutocompletePermission());
permissions = builder.build();
}
@@ -264,6 +268,14 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
return info;
}
private String getGroupAutocompletePermission() {
return GroupPermissions.autocomplete().asShiroString();
}
private String getUserAutocompletePermission() {
return UserPermissions.autocomplete().asShiroString();
}
private String canReadOwnUser(User user) {
return UserPermissions.read(user.getName()).asShiroString();
}

View File

@@ -300,6 +300,12 @@ public class DefaultUserManager extends AbstractUserManager
return getAll(null);
}
@Override
public Collection<User> autocomplete(String filter) {
UserPermissions.autocomplete().check();
return search(new SearchRequest(filter,true, DEFAULT_LIMIT));
}
/**
* Method description
*

View File

@@ -0,0 +1,229 @@
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.SubjectThreadState;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.util.ThreadState;
import org.assertj.core.util.Lists;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.Manager;
import sonia.scm.group.DefaultGroupManager;
import sonia.scm.group.Group;
import sonia.scm.group.GroupManager;
import sonia.scm.group.xml.XmlGroupDAO;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.user.DefaultUserManager;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.xml.XmlUserDAO;
import sonia.scm.web.VndMediaType;
import sonia.scm.xml.XmlDatabase;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
@RunWith(MockitoJUnitRunner.Silent.class)
public class AutoCompleteResourceTest {
public static final String URL = "/" + AutoCompleteResource.PATH;
private final Integer defaultLimit = Manager.DEFAULT_LIMIT;
private Dispatcher dispatcher;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
private XmlUserDAO userDao;
private XmlGroupDAO groupDao;
private XmlDatabase xmlDB;
private ObjectMapper jsonObjectMapper = new ObjectMapper();
@Before
public void prepareEnvironment() {
initMocks(this);
ConfigurationStoreFactory storeFactory = mock(ConfigurationStoreFactory.class);
ConfigurationStore<Object> storeConfig = mock(ConfigurationStore.class);
xmlDB = mock(XmlDatabase.class);
when(storeConfig.get()).thenReturn(xmlDB);
when(storeFactory.getStore(any(), any())).thenReturn(storeConfig);
XmlUserDAO userDao = new XmlUserDAO(storeFactory);
this.userDao = spy(userDao);
XmlGroupDAO groupDAO = new XmlGroupDAO(storeFactory);
groupDao = spy(groupDAO);
ReducedObjectModelToDtoMapperImpl mapper = new ReducedObjectModelToDtoMapperImpl();
UserManager userManager = new DefaultUserManager(this.userDao);
GroupManager groupManager = new DefaultGroupManager(groupDao);
AutoCompleteResource autoCompleteResource = new AutoCompleteResource(mapper, userManager, groupManager);
dispatcher = createDispatcher(autoCompleteResource);
subjectThreadState.bind();
ThreadContext.bind(subject);
when(subject.isPermitted(any(String.class))).thenReturn(true);
}
@After
public void cleanupContext() {
ThreadContext.unbindSubject();
}
@Test
public void shouldGet400OnFailedParameterForUserSearch() throws Exception {
MockHttpRequest request = MockHttpRequest
.get("/" + AutoCompleteResource.PATH + "user")
.contentType(VndMediaType.AUTOCOMPLETE)
.accept(VndMediaType.AUTOCOMPLETE);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(400, response.getStatus());
}
@Test
public void shouldGet400IfParameterLengthLessThan2CharsForUserSearch() throws Exception {
MockHttpRequest request = MockHttpRequest
.get("/" + AutoCompleteResource.PATH + "user?filter=a")
.contentType(VndMediaType.AUTOCOMPLETE)
.accept(VndMediaType.AUTOCOMPLETE);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(400, response.getStatus());
}
@Test
public void shouldSearchUsers() throws Exception {
ArrayList<User> users = Lists.newArrayList(createMockUser("YuCantFindMe", "ha ha"), createMockUser("user1", "User 1"), createMockUser("user2", "User 2"));
String searched = "user";
when(xmlDB.values()).thenReturn(users);
MockHttpRequest request = MockHttpRequest
.get("/" + AutoCompleteResource.PATH + "user?filter=" + searched)
.contentType(VndMediaType.AUTOCOMPLETE)
.accept(VndMediaType.AUTOCOMPLETE);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertResultSize(response, 2);
assertTrue(response.getContentAsString().contains("\"id\":\"user1\""));
assertTrue(response.getContentAsString().contains("\"displayName\":\"User 1\""));
assertTrue(response.getContentAsString().contains("\"id\":\"user2\""));
assertTrue(response.getContentAsString().contains("\"displayName\":\"User 2\""));
}
@Test
public void shouldSearchUsersWithDefaultLimitLength() throws Exception {
List<User> userList = IntStream.range(0, 10).boxed().map(i -> createMockUser("user" + i, "User " + i)).collect(Collectors.toList());
when(xmlDB.values()).thenReturn(userList);
MockHttpRequest request = MockHttpRequest
.get("/" + AutoCompleteResource.PATH + "user?filter=user")
.contentType(VndMediaType.AUTOCOMPLETE)
.accept(VndMediaType.AUTOCOMPLETE);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertResultSize(response, defaultLimit);
}
@Test
public void shouldGet400OnFailedParameterForGroupSearch() throws Exception {
MockHttpRequest request = MockHttpRequest
.get("/" + AutoCompleteResource.PATH + "group")
.contentType(VndMediaType.AUTOCOMPLETE)
.accept(VndMediaType.AUTOCOMPLETE);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(400, response.getStatus());
}
@Test
public void shouldGet400IfParameterLengthLessThan2CharsForGroupSearch() throws Exception {
MockHttpRequest request = MockHttpRequest
.get("/" + AutoCompleteResource.PATH + "group?filter=a")
.contentType(VndMediaType.AUTOCOMPLETE)
.accept(VndMediaType.AUTOCOMPLETE);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(400, response.getStatus());
}
@Test
public void shouldSearchGroups() throws Exception {
ArrayList<Group> groups = Lists.newArrayList(createMockGroup("YuCantFindMe"), createMockGroup("group_1"), createMockGroup("group_2"));
String searched = "group";
when(xmlDB.values()).thenReturn(groups);
MockHttpRequest request = MockHttpRequest
.get("/" + AutoCompleteResource.PATH + "group?filter=" + searched)
.contentType(VndMediaType.AUTOCOMPLETE)
.accept(VndMediaType.AUTOCOMPLETE);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertResultSize(response, 2);
assertTrue(response.getContentAsString().contains("\"id\":\"group_1\""));
assertTrue(response.getContentAsString().contains("\"displayName\":\"group_1\""));
assertTrue(response.getContentAsString().contains("\"id\":\"group_2\""));
assertTrue(response.getContentAsString().contains("\"displayName\":\"group_2\""));
}
@Test
public void shouldSearchGroupsWithDefaultLimitLength() throws Exception {
List<Group> groups = IntStream.range(0, 10).boxed().map(i -> createMockGroup("group_" + i)).collect(Collectors.toList());
when(xmlDB.values()).thenReturn(groups);
MockHttpRequest request = MockHttpRequest
.get("/" + AutoCompleteResource.PATH + "group?filter=group")
.contentType(VndMediaType.AUTOCOMPLETE)
.accept(VndMediaType.AUTOCOMPLETE);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertResultSize(response, defaultLimit);
}
private User createMockUser(String id, String name) {
return new User(id, name, "em@l.de");
}
private Group createMockGroup(String name) {
Group group = new Group("type", name);
group.setDescription(name);
return group;
}
private void assertResultSize(MockHttpResponse response, int size) throws java.io.IOException {
ReducedObjectModelDto[] reducedObjectModelDtos = jsonObjectMapper.readValue(response.getContentAsString(), ReducedObjectModelDto[].class);
assertEquals(reducedObjectModelDtos.length, size);
}
}

View File

@@ -23,7 +23,6 @@ import javax.servlet.http.HttpServletResponse;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.MessageFormat;
import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals;

View File

@@ -161,8 +161,8 @@ public class DefaultAuthorizationCollectorTest {
AuthorizationInfo authInfo = collector.collect();
assertThat(authInfo.getRoles(), Matchers.contains(Role.USER));
assertThat(authInfo.getStringPermissions(), hasSize(1));
assertThat(authInfo.getStringPermissions(), contains("user:read:trillian"));
assertThat(authInfo.getStringPermissions(), hasSize(3));
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:read:trillian"));
assertThat(authInfo.getObjectPermissions(), nullValue());
}
@@ -209,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", "user:read:trillian"));
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian"));
}
/**
@@ -230,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", "user:read:trillian"));
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian", "user:autocomplete" , "group:autocomplete" ));
}
private void authenticate(User user, String group, String... groups) {