Commit dc1464cc authored by Maxym Borodenko's avatar Maxym Borodenko
Browse files

User administration API

parent ec6fc69b
/*
* Copyright 2017 Global Crop Diversity Trust
* Copyright 2018 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -21,13 +21,14 @@ import org.genesys.catalog.model.user.User;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.stereotype.Repository;
/**
* The Interface UserRepository.
*/
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
public interface UserRepository extends JpaRepository<User, Long>, QueryDslPredicateExecutor<User> {
/**
* Find by email.
......@@ -38,6 +39,15 @@ public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where lower(u.email) = lower(?1)")
User findByEmail(String email);
/**
* Find by uuid and version.
*
* @param uuid the uuid
* @param version the version
* @return the user
*/
User getByUuidAndVersion(String uuid, int version);
/**
* Autocomplete user by email or fullName
*
......
......@@ -16,10 +16,15 @@
package org.genesys.catalog.service;
import java.util.List;
import java.util.UUID;
import org.genesys.blocks.security.service.BasicUserService;
import org.genesys.catalog.model.user.User;
import org.genesys.catalog.model.user.UserRole;
import org.genesys.catalog.service.filters.UserFilter;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
/**
* The Interface UserService.
......@@ -35,4 +40,23 @@ public interface UserService extends BasicUserService<UserRole, User> {
*/
List<User> autocompleteUser(String term, int limit);
/**
* Gets the user.
*
* @param uuid the uuid
* @param version the version
* @return the user
*/
User getUser(UUID uuid, int version);
/**
* List users matching the filter.
*
* @param filter filter data
* @param page page
* @return list of User
*/
@PreAuthorize("hasRole('ADMINISTRATOR')")
Page<User> listUsers(UserFilter filter, Pageable page);
}
/*
* Copyright 2018 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.catalog.service.filters;
import static org.genesys.catalog.model.user.QUser.user;
import java.util.Set;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.DateTimeExpression;
import org.apache.commons.collections4.CollectionUtils;
import org.genesys.blocks.model.filters.AuditedVersionedModelFilter;
import org.genesys.blocks.model.filters.StringFilter;
import org.genesys.blocks.security.model.BasicUser;
import org.genesys.catalog.model.user.UserRole;
/**
* The Class UserFilter.
*
* @author Maxym Borodenko
*/
public class UserFilter extends AuditedVersionedModelFilter {
/**
* The full name.
*/
public StringFilter fullName;
/**
* The email.
*/
public StringFilter email;
/**
* The user role.
*/
public Set<UserRole> role;
/**
* The account type.
*/
public Set<BasicUser.AccountType> accountType;
/**
* The locked.
*/
public Boolean locked;
/**
* The expired.
*/
public Boolean expired;
/**
* The passwordExpired.
*/
public Boolean passwordExpired;
/**
* Builds the query.
*
* @return the predicate
*/
public Predicate buildQuery() {
final BooleanBuilder and = new BooleanBuilder();
super.buildQuery(user._super._super._super, and);
if (fullName != null) {
and.and(fullName.buildQuery(user.fullName));
}
if (email != null) {
and.and(email.buildQuery(user.email));
}
if ((accountType != null) && !accountType.isEmpty()) {
and.and(user.accountType.in(accountType));
}
if (CollectionUtils.isNotEmpty(role)) {
and.and(user.roles.any().in(role));
}
if (locked != null && locked) {
and.and(user.lockedUntil.gt(DateTimeExpression.currentDate()));
}
if (expired != null && expired) {
and.and(user.accountExpires.lt(DateTimeExpression.currentDate()));
}
if (passwordExpired != null && passwordExpired) {
and.and(user.passwordExpires.lt(DateTimeExpression.currentDate()));
}
return and;
}
}
......@@ -17,6 +17,7 @@ package org.genesys.catalog.service.impl;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.security.NotUniqueUserException;
......@@ -28,10 +29,14 @@ import org.genesys.catalog.model.user.User;
import org.genesys.catalog.model.user.UserRole;
import org.genesys.catalog.persistence.user.UserRepository;
import org.genesys.catalog.service.UserService;
import org.genesys.catalog.service.filters.UserFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
......@@ -104,4 +109,17 @@ public class UserServiceImpl extends BasicUserServiceImpl<UserRole, User> implem
return Collections.emptyList();
return userRepository.autocomplete(email, new PageRequest(0, Integer.min(100, limit), new Sort("email")));
}
@Override
public User getUser(final UUID uuid, final int version) {
final User user = userRepository.getByUuidAndVersion(uuid.toString(), version);
if (user == null)
throw new ConcurrencyFailureException("Record with that version doesn't exist");
return deepLoad(user);
}
@Override
public Page<User> listUsers(final UserFilter filter, final Pageable page) {
return userRepository.findAll(filter.buildQuery(), page);
}
}
/*
* Copyright 2018 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.catalog.util;
import java.util.Random;
/**
* @author Maxym Borodenko
*/
public class RandomPasswordUtil {
private static final String symbols = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$&@?~!%#";
/**
* This method generates a random password that consists of at least one special character, one number,
* one lowercase letter and one uppercase letter
*
* @param random
* @param length the password length that should be generated
* @return string with generated password
* @throws IllegalArgumentException if the 'length' parameter is lower than 4 characters
*/
public static String generatePassword(final Random random, final int length) {
if (length < 4) {
throw new IllegalArgumentException("Password must be at least 4 characters");
}
while (true) {
char[] password = new char[length];
boolean hasUpper = false, hasLower = false, hasDigit = false, hasSpecial = false;
for (int i = 0; i < password.length; i++) {
char ch = symbols.charAt(random.nextInt(symbols.length()));
if (Character.isUpperCase(ch))
hasUpper = true;
else if (Character.isLowerCase(ch))
hasLower = true;
else if (Character.isDigit(ch))
hasDigit = true;
else
hasSpecial = true;
password[i] = ch;
}
if (hasUpper && hasLower && hasDigit && hasSpecial) {
return new String(password);
}
}
}
}
/*
* Copyright 2018 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.catalog.server.controller.api.v0.admin;
import java.util.Random;
import java.util.UUID;
import org.genesys.blocks.security.UserException;
import org.genesys.catalog.model.user.User;
import org.genesys.catalog.service.UserService;
import org.genesys.catalog.service.filters.UserFilter;
import org.genesys.catalog.util.RandomPasswordUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* The Class UserAdminController.
*
* @author Maxym Borodenko
*/
@RestController
@RequestMapping(UserAdminController.API_BASE)
@PreAuthorize("hasRole('ADMINISTRATOR')")
public class UserAdminController {
protected static final String API_BASE = "/api/v0/admin";
private static final Logger LOG = LoggerFactory.getLogger(UserAdminController.class);
@Autowired
private UserService userService;
@PostMapping(value = "/create")
public User addUser(@RequestBody final User user) throws UserException {
final String password = RandomPasswordUtil.generatePassword(new Random(), 15);
return userService.createUser(user.getEmail(), user.getFullName(), password, user.getAccountType());
}
@PostMapping(value = "/update")
public User updaterUser(@RequestBody final User user) throws UserException {
return userService.updateUser(user, user.getEmail(), user.getFullName());
}
@DeleteMapping(value = "/delete/{UUID},{version}")
public User deleteUser(@PathVariable("UUID") final UUID uuid, @PathVariable("version") final int version) {
final User user = userService.getUser(uuid, version);
userService.deleteUser(user);
user.setId(null);
return user;
}
@PostMapping(value = "/list")
public Page<User> list(@RequestParam(name = "p", required = false, defaultValue = "0") final int page,
@RequestParam(name = "l", required = false, defaultValue = "50") final int pageSize,
@RequestParam(name = "d", required = false, defaultValue = "ASC") final Sort.Direction direction,
@RequestParam(name = "s", required = false, defaultValue = "id") final String[] sort,
@RequestBody final UserFilter filter) {
return userService.listUsers(filter, new PageRequest(page, Integer.min(pageSize, 100), direction, sort));
}
}
/*
* Copyright 2018 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.catalog.server.controller.api.v0.admin;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertNull;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Random;
import org.genesys.blocks.model.filters.StringFilter;
import org.genesys.blocks.security.model.BasicUser;
import org.genesys.catalog.model.user.User;
import org.genesys.catalog.persistence.user.UserRepository;
import org.genesys.catalog.server.controller.api.v0.AbstractDatasetControllerTest;
import org.genesys.catalog.server.controller.rest.WithMockOAuth2Authentication;
import org.genesys.catalog.service.filters.UserFilter;
import org.genesys.catalog.util.RandomPasswordUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
/**
* @author Maxym Borodenko
*/
public class UserAdminControllerTest extends AbstractDatasetControllerTest {
private static final String EMAIL = "test@email.com";
private static final String EMAIL_2 = "test2@email.com";
private static final String FULL_NAME = "Test full name";
private static final String FULL_NAME_2 = "Test full name 2";
private static final BasicUser.AccountType ACCOUNT_TYPE = BasicUser.AccountType.LOCAL;
@Autowired
private UserRepository userRepository;
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@Before
@Transactional
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).apply(documentationConfiguration(restDocumentation).uris().withScheme("https").withHost(
"api.catalog.genesys-pgr.org").withPort(443)).build();
}
@After
@Override
public void cleanup() {
userRepository.deleteAll();
super.cleanup();
}
@Test
@WithMockOAuth2Authentication(roles = {"ADMINISTRATOR"})
public void createUserTest() throws Exception {
final String s = verboseMapper.writeValueAsString(setUser());
/*@formatter:off*/
mockMvc
.perform(RestDocumentationRequestBuilders.post(UserAdminController.API_BASE + "/create")
.content(s)
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
// .andDo(MockMvcResultHandlers.print())
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(jsonPath("$", not(nullValue())))
.andExpect(jsonPath("$.email", is(EMAIL)))
.andExpect(jsonPath("$.accountType", is(ACCOUNT_TYPE.toString())))
.andExpect(jsonPath("$.fullName", is(FULL_NAME)));
/*@formatter:on*/
}
@Test
@WithMockOAuth2Authentication(roles = {"ADMINISTRATOR"})
public void updateUserTest() throws Exception {
User user = setUser();
final String password = RandomPasswordUtil.generatePassword(new Random(), 15);
user.setPassword(password);
user = userRepository.save(user);
user.setEmail(EMAIL_2);
user.setFullName(FULL_NAME_2);
String s = verboseMapper.writeValueAsString(user);
/*@formatter:off*/
mockMvc
.perform(
RestDocumentationRequestBuilders
.post(UserAdminController.API_BASE + "/update")
.content(s)
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultHandlers.print())
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(jsonPath("$", not(nullValue())))
.andExpect(jsonPath("$.email", is(EMAIL_2)))
.andExpect(jsonPath("$.fullName", is(FULL_NAME_2)));
/*@formatter:on*/
}
@Test
@WithMockOAuth2Authentication(roles = {"ADMINISTRATOR"})
public void deleteUserTest() throws Exception {
User user = setUser();
final String password = RandomPasswordUtil.generatePassword(new Random(), 15);
user.setPassword(password);
user = userRepository.save(user);
/*@formatter:off*/
mockMvc
.perform(RestDocumentationRequestBuilders
.delete(UserAdminController.API_BASE.concat("/delete/{UUID},{version}"), user.getUuid(), user.getVersion())
.contentType(MediaType.APPLICATION_JSON))
// .andDo(MockMvcResultHandlers.print())
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8));
/*@formatter:on*/
assertNull(userRepository.findOne(user.getId()));
}
@Test
@WithMockUser(username = "user", password = "user", roles = "ADMINISTRATOR")
public void listUsers() throws Exception {
UserFilter userFilter = new UserFilter();
StringFilter stringFilter = new StringFilter();
stringFilter.eq = FULL_NAME;
userFilter.fullName = stringFilter;
User user = setUser();
final String password = RandomPasswordUtil.generatePassword(new Random(), 15);
user.setPassword(password);
user = userRepository.save(user);
User user2 = setUser(FULL_NAME_2, EMAIL_2, ACCOUNT_TYPE);
final String password2 = RandomPasswordUtil.generatePassword(new Random(), 15);
user2.setPassword(password2);
user2 = userRepository.save(user2);
/*@formatter:off*/
mockMvc
.perform(RestDocumentationRequestBuilders.post(UserAdminController.API_BASE.concat("/list"))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(userFilter)))
// .andDo(MockMvcResultHandlers.print())
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(jsonPath("$.content[0]", not(nullValue())))
.andExpect(jsonPath("$.content[0].id", is(user.getId().intValue())));
/*@formatter:on*/
}
protected User setUser() {
return setUser(FULL_NAME, EMAIL, ACCOUNT_TYPE);
}
protected User setUser(final String fullName, final String email, final BasicUser.AccountType accountType) {
final User user = new User();