Commit 053ddd0f authored by Administrator's avatar Administrator

Adds support for retrieving the data of an user

This commit adds the new resource UsersResource (/users) with a simple
functionality: retrieving the data of an user. This functionality
requires knowing the login of the user that is performing the request.
In this regard, this commit is an example of how this information can be
retrieved and how to test a resource that requires knowing the login of
an user.
parent a6de3e68
......@@ -24,4 +24,6 @@ INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'María','Nu
INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Alba','Fernández');
INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Asunción','Jiménez');
INSERT INTO `daaexample`.`users` (`login`,`password`) VALUES ('admin', '0b893644f3b2097d004c58d585e784ac92dd1356d25158a298573ad54ab2d15d');
-- The password for each user is its login suffixed with "pass". For example, user "admin" has the password "adminpass".
INSERT INTO `daaexample`.`users` (`login`,`password`) VALUES ('admin', '43f413b773f7d0cfad0e8e6529ec1249ce71e8697919eab30d82d800a3986b70');
INSERT INTO `daaexample`.`users` (`login`,`password`) VALUES ('normal', '688f21dd2d65970f174e2c9d35159250a8a23e27585452683db8c5d10b586336');
package es.uvigo.esei.daa;
import static java.util.stream.Collectors.toSet;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import es.uvigo.esei.daa.rest.PeopleResource;
import es.uvigo.esei.daa.rest.UsersResource;
/**
* Configuration of the REST application. This class includes the resources and
......@@ -22,8 +24,8 @@ import es.uvigo.esei.daa.rest.PeopleResource;
public class DAAExampleApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
return Stream.of(PeopleResource.class)
.collect(Collectors.toSet());
return Stream.of(PeopleResource.class, UsersResource.class)
.collect(toSet());
}
@Override
......
......@@ -30,11 +30,13 @@ import es.uvigo.esei.daa.dao.UsersDAO;
* @author Miguel Reboiro Jato
*
*/
@WebFilter(urlPatterns = { "/*", "/logout" })
@WebFilter(urlPatterns = "/*")
public class LoginFilter implements Filter {
private static final String REST_PATH = "/rest";
private static final String INDEX_PATH = "/index.html";
private static final String LOGOUT_PATH = "/logout";
private static final String REST_PATH = "/rest";
private static final String[] PUBLIC_PATHS = new String[] {
"/index.html" // Add the paths that can be publicly accessed (e.g. /js, /css...)
};
@Override
public void doFilter(
......@@ -48,15 +50,15 @@ public class LoginFilter implements Filter {
try {
if (isLogoutPath(httpRequest)) {
destroySession(httpRequest);
removeTokenCookie(httpResponse);
removeTokenCookie(httpRequest, httpResponse);
redirectToIndex(httpRequest, httpResponse);
} else if (isIndexPath(httpRequest) || checkToken(httpRequest)) {
} else if (isPublicPath(httpRequest) || checkToken(httpRequest)) {
chain.doFilter(request, response);
} else if (checkLogin(httpRequest, httpResponse)) {
continueWithRedirect(httpRequest, httpResponse);
} else if (isRestPath(httpRequest)) {
destroySession(httpRequest);
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
} else {
destroySession(httpRequest);
redirectToIndex(httpRequest, httpResponse);
......@@ -80,8 +82,13 @@ public class LoginFilter implements Filter {
return request.getServletPath().equals(LOGOUT_PATH);
}
private boolean isIndexPath(HttpServletRequest request) {
return request.getServletPath().equals(INDEX_PATH);
private boolean isPublicPath(HttpServletRequest request) {
for (String path : PUBLIC_PATHS) {
if (request.getServletPath().startsWith(path))
return true;
}
return false;
}
private boolean isRestPath(HttpServletRequest request) {
......@@ -106,10 +113,15 @@ public class LoginFilter implements Filter {
response.sendRedirect(redirectPath);
}
private void removeTokenCookie(HttpServletResponse response) {
final Cookie cookie = new Cookie("token", "");
cookie.setMaxAge(0);
response.addCookie(cookie);
private void removeTokenCookie(
HttpServletRequest request,
HttpServletResponse response
) {
final Cookie cookie = getTokenCookie(request);
if (cookie != null) {
cookie.setMaxAge(0);
response.addCookie(cookie);
}
}
private void destroySession(HttpServletRequest request) {
......@@ -125,6 +137,7 @@ public class LoginFilter implements Filter {
if (login != null && password != null) {
final UsersDAO dao = new UsersDAO();
if (dao.checkLogin(login, password)) {
final Credentials credentials = new Credentials(login, password);
......@@ -140,18 +153,33 @@ public class LoginFilter implements Filter {
}
}
private boolean checkToken(HttpServletRequest request)
throws DAOException, IllegalArgumentException {
private Cookie getTokenCookie(HttpServletRequest request) {
final Cookie[] cookies = Optional.ofNullable(request.getCookies())
.orElse(new Cookie[0]);
for (Cookie cookie : cookies) {
if ("token".equals(cookie.getName())) {
final Credentials credentials = new Credentials(cookie.getValue());
final UsersDAO dao = new UsersDAO();
return dao.checkLogin(credentials.getLogin(), credentials.getPassword());
return cookie;
}
}
return null;
}
private boolean checkToken(HttpServletRequest request)
throws DAOException, IllegalArgumentException {
final Cookie cookie = getTokenCookie(request);
if (cookie != null) {
final Credentials credentials = new Credentials(cookie.getValue());
final UsersDAO dao = new UsersDAO();
if (dao.checkLogin(credentials.getLogin(), credentials.getPassword())) {
request.getSession().setAttribute("login", credentials.getLogin());
return true;
} else {
return false;
}
}
......
......@@ -9,6 +9,8 @@ import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;
import es.uvigo.esei.daa.entities.User;
/**
* DAO class for managing the users of the system.
*
......@@ -18,35 +20,28 @@ public class UsersDAO extends DAO {
private final static Logger LOG = Logger.getLogger(UsersDAO.class.getName());
private final static String SALT = "daaexample-";
/**
* Checks if the provided credentials (login and password) correspond with a
* valid user registered in the system.
* Returns a user stored persisted in the system.
*
* <p>The password is stored in the system "salted" and encoded with the
* SHA-256 algorithm.</p>
*
* @param login the login of the user.
* @param password the password of the user.
* @return {@code true} if the credentials are valid. {@code false}
* otherwise.
* @throws DAOException if an error happens while checking the credentials.
* @param login the login of the user to be retrieved.
* @return a user with the provided login.
* @throws DAOException if an error happens while retrieving the user.
* @throws IllegalArgumentException if the provided login does not
* corresponds with any persisted user.
*/
public boolean checkLogin(String login, String password) throws DAOException {
public User get(String login) throws DAOException {
try (final Connection conn = this.getConnection()) {
final String query = "SELECT password FROM users WHERE login=?";
final String query = "SELECT * FROM users WHERE login=?";
try (final PreparedStatement statement = conn.prepareStatement(query)) {
statement.setString(1, login);
try (final ResultSet result = statement.executeQuery()) {
if (result.next()) {
final String dbPassword = result.getString("password");
final String shaPassword = encodeSha256(SALT + password);
return shaPassword.equals(dbPassword);
return rowToEntity(result);
} else {
return false;
throw new IllegalArgumentException("Invalid id");
}
}
}
......@@ -55,6 +50,28 @@ public class UsersDAO extends DAO {
throw new DAOException(e);
}
}
/**
* Checks if the provided credentials (login and password) correspond with a
* valid user registered in the system.
*
* <p>The password is stored in the system "salted" and encoded with the
* SHA-256 algorithm.</p>
*
* @param login the login of the user.
* @param password the password of the user.
* @return {@code true} if the credentials are valid. {@code false}
* otherwise.
* @throws DAOException if an error happens while checking the credentials.
*/
public boolean checkLogin(String login, String password) throws DAOException {
final User user = this.get(login);
final String dbPassword = user.getPassword();
final String shaPassword = encodeSha256(SALT + password);
return shaPassword.equals(dbPassword);
}
private final static String encodeSha256(String text) {
try {
......@@ -77,4 +94,11 @@ public class UsersDAO extends DAO {
return sb.toString();
}
private User rowToEntity(ResultSet result) throws SQLException {
return new User(
result.getString("login"),
result.getString("password")
);
}
}
package es.uvigo.esei.daa.entities;
import static java.util.Objects.requireNonNull;
/**
* An entity that represents a user.
*
* @author Miguel Reboiro Jato
*/
public class User {
private String login;
private String password;
// Constructor needed for the JSON conversion
User() {}
/**
* Constructs a new instance of {@link User}.
*
* @param login login that identifies the user in the system.
* @param password password of the user encoded using SHA-256 and with the
* "salt" prefix added.
*/
public User(String login, String password) {
this.setLogin(login);
this.setPassword(password);
}
/**
* Returns the login of the user.
*
* @return the login of the user.
*/
public String getLogin() {
return login;
}
/**
* Sets the login of the user.
*
* @param login the login that identifies the user in the system.
*/
public void setLogin(String login) {
this.login = requireNonNull(login, "Login can't be null");
}
/**
* Returns the password of the user.
*
* @return the password of the user.
*/
public String getPassword() {
return password;
}
/**
* Sets the users password.
* @param password the password of the user encoded using SHA-256 and with
* the "salt" prefix added.
*/
public void setPassword(String password) {
requireNonNull(password, "Password can't be null");
if (!password.matches("[a-zA-Z0-9]{64}"))
throw new IllegalArgumentException("Password must be a valid SHA-256");
this.password = password;
}
}
package es.uvigo.esei.daa.rest;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import es.uvigo.esei.daa.dao.DAOException;
import es.uvigo.esei.daa.dao.UsersDAO;
/**
* REST resource for managing users.
*
* @author Miguel Reboiro Jato.
*/
@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
public class UsersResource {
private final static Logger LOG = Logger.getLogger(UsersResource.class.getName());
private final UsersDAO dao;
// The HttpServletRequest can be also injected as a parameter in the REST
// methods.
private @Context HttpServletRequest request;
/**
* Constructs a new instance of {@link UsersResource}.
*/
public UsersResource() {
this(new UsersDAO());
}
// Needed for testing purposes
UsersResource(UsersDAO dao) {
this(dao, null);
}
// Needed for testing purposes
UsersResource(UsersDAO dao, HttpServletRequest request) {
this.dao = dao;
this.request = request;
}
/**
* Returns a user with the provided login.
*
* @param login the identifier of the user to retrieve.
* @return a 200 OK response with an user that has the provided login.
* If the request is done without providing the login credentials or using
* invalid credentials a 401 Unauthorized response will be returned. If the
* credentials are provided and a regular user (i.e. non admin user) tries
* to access the data of other user, a 403 Forbidden response will be
* returned. If the credentials are OK, but the login does not corresponds
* with any user, a 400 Bad Request response with an error message will be
* returned. If an error happens while retrieving the list, a 500 Internal
* Server Error response with an error message will be returned.
*/
@GET
@Path("/{login}")
public Response get(
@PathParam("login") String login
) {
final String loggedUser = getLogin();
// Each user can only access his or her own data. Only the admin user
// can access the data of any user.
if (loggedUser.equals(login) || loggedUser.equals("admin")) {
try {
return Response.ok(dao.get(login)).build();
} catch (IllegalArgumentException iae) {
LOG.log(Level.FINE, "Invalid user login in get method", iae);
return Response.status(Response.Status.BAD_REQUEST)
.entity(iae.getMessage())
.build();
} catch (DAOException e) {
LOG.log(Level.SEVERE, "Error getting an user", e);
return Response.serverError()
.entity(e.getMessage())
.build();
}
} else {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
}
private String getLogin() {
return (String) request.getSession().getAttribute("login");
}
}
package es.uvigo.esei.daa.dataset;
import java.util.Arrays;
import java.util.Base64;
import es.uvigo.esei.daa.entities.User;
public final class UsersDataset {
private UsersDataset() {}
public static User[] users() {
return new User[] {
new User(adminLogin(), "43f413b773f7d0cfad0e8e6529ec1249ce71e8697919eab30d82d800a3986b70"),
new User(normalLogin(), "688f21dd2d65970f174e2c9d35159250a8a23e27585452683db8c5d10b586336")
};
}
public static User user(String login) {
return Arrays.stream(users())
.filter(user -> user.getLogin().equals(login))
.findAny()
.orElseThrow(IllegalArgumentException::new);
}
public static String adminLogin() {
return "admin";
}
public static String normalLogin() {
return "normal";
}
public static String userToken(String login) {
final String chain = login + ":" + login + "pass";
return Base64.getEncoder().encodeToString(chain.getBytes());
}
}
......@@ -77,4 +77,14 @@ public class HasHttpStatus extends TypeSafeMatcher<Response> {
public static Matcher<Response> hasInternalServerErrorStatus() {
return new HasHttpStatus(Response.Status.INTERNAL_SERVER_ERROR);
}
@Factory
public static Matcher<Response> hasUnauthorized() {
return new HasHttpStatus(Response.Status.UNAUTHORIZED);
}
@Factory
public static Matcher<Response> hasForbidden() {
return new HasHttpStatus(Response.Status.FORBIDDEN);
}
}
package es.uvigo.esei.daa.matchers;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import es.uvigo.esei.daa.entities.Person;
import es.uvigo.esei.daa.entities.User;
public class IsEqualToUser extends IsEqualToEntity<User> {
public IsEqualToUser(User entity) {
super(entity);
}
@Override
protected boolean matchesSafely(User actual) {
this.clearDescribeTo();
if (actual == null) {
this.addTemplatedDescription("actual", expected.toString());
return false;
} else {
return checkAttribute("login", User::getLogin, actual)
&& checkAttribute("password", User::getPassword, actual);
}
}
/**
* Factory method that creates a new {@link IsEqualToEntity} matcher with
* the provided {@link Person} as the expected value.
*
* @param user the expected person.
* @return a new {@link IsEqualToEntity} matcher with the provided
* {@link Person} as the expected value.
*/
@Factory
public static IsEqualToUser equalsToUser(User user) {
return new IsEqualToUser(user);
}
/**
* Factory method that returns a new {@link Matcher} that includes several
* {@link IsEqualToUser} matchers, each one using an {@link Person} of the
* provided ones as the expected value.
*
* @param users the persons to be used as the expected values.
* @return a new {@link Matcher} that includes several
* {@link IsEqualToUser} matchers, each one using an {@link Person} of the
* provided ones as the expected value.
* @see IsEqualToEntity#containsEntityInAnyOrder(java.util.function.Function, Object...)
*/
@Factory
public static Matcher<Iterable<? extends User>> containsPeopleInAnyOrder(User ... users) {
return containsEntityInAnyOrder(IsEqualToUser::equalsToUser, users);
}
}
package es.uvigo.esei.daa.rest;
import static es.uvigo.esei.daa.dataset.UsersDataset.adminLogin;
import static es.uvigo.esei.daa.dataset.UsersDataset.normalLogin;
import static es.uvigo.esei.daa.dataset.UsersDataset.user;
import static es.uvigo.esei.daa.dataset.UsersDataset.userToken;
import static es.uvigo.esei.daa.matchers.HasHttpStatus.hasForbidden;
import static es.uvigo.esei.daa.matchers.HasHttpStatus.hasOkStatus;
import static es.uvigo.esei.daa.matchers.HasHttpStatus.hasUnauthorized;
import static es.uvigo.esei.daa.matchers.IsEqualToUser.equalsToUser;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.io.IOException;
import javax.sql.DataSource;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Response;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.glassfish.jersey.test.DeploymentContext;
import org.glassfish.jersey.test.JerseyTest;
import org.glassfish.jersey.test.ServletDeploymentContext;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.glassfish.jersey.test.spi.TestContainerFactory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import es.uvigo.esei.daa.DAAExampleApplication;
import es.uvigo.esei.daa.LoginFilter;
import es.uvigo.esei.daa.entities.User;
import es.uvigo.esei.daa.listeners.ApplicationContextBinding;
import es.uvigo.esei.daa.listeners.ApplicationContextJndiBindingTestExecutionListener;
import es.uvigo.esei.daa.listeners.DbManagement;
import es.uvigo.esei.daa.listeners.DbManagementTestExecutionListener;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:contexts/mem-context.xml")
@TestExecutionListeners({
DbUnitTestExecutionListener.class,
DbManagementTestExecutionListener.class,
ApplicationContextJndiBindingTestExecutionListener.class
})
@ApplicationContextBinding(
jndiUrl = "java:/comp/env/jdbc/daaexample",
type = DataSource.class
)
@DbManagement(
create = "classpath:db/hsqldb.sql",
drop = "classpath:db/hsqldb-drop.sql"
)
@DatabaseSetup("/datasets/dataset.xml")
@ExpectedDatabase("/datasets/dataset.xml")
public class UsersResourceTest extends JerseyTest {
@Override
protected Application configure() {
return new DAAExampleApplication();
}
@Override
protected void configureClient(ClientConfig config) {
super.configureClient(config);
// Enables JSON transformation in client
config.register(JacksonJsonProvider.class);
config.property("com.sun.jersey.api.json.POJOMappingFeature", Boolean.TRUE);
}
@Override
protected TestContainerFactory getTestContainerFactory() {
return new GrizzlyWebTestContainerFactory();
}
@Override
protected DeploymentContext configureDeployment() {
return ServletDeploymentContext.forServlet(
new ServletContainer(ResourceConfig.forApplication(configure()))
)
.servletPath("/rest")
.addFilter(LoginFilter.class, "login-filter")
.build();
}
@Test
public void testGetAdminOwnUser() throws IOException {
final String admin = adminLogin();
final Response response = target("users/" + admin).request()
.cookie("token", userToken(admin))
.get();
assertThat(response, hasOkStatus());
final User user = response.readEntity(User.class);
assertThat(user, is(equalsToUser(user(admin))));
}
@Test
public void testGetAdminOtherUser() throws IOException {
final String admin = adminLogin();
final String otherUser = normalLogin();
final Response response = target("users/" + otherUser).request()
.cookie("token", userToken(admin))
.get();
assertThat(response, hasOkStatus());
final User user = response.readEntity(User.class);
assertThat(user, is(equalsToUser(user(otherUser))));
}
@Test
public void testGetNormalOwnUser() throws IOException {
final String login = normalLogin();
final Response response = target("users/" + login).request()
.cookie("token", userToken(login))
.get();
assertThat(response, hasOkStatus());
final User user = response.readEntity(User.class);
assertThat(user, is(equalsToUser(user(login))));
}
@Test
public void testGetNoCredentials() throws IOException {
final Response response = target("users/" + normalLogin()).request().get();
assertThat(response, hasForbidden());
}
@Test
public void testGetBadCredentials() throws IOException {
final Response response = target("users/" + adminLogin()).request()
.cookie("token", "YmFkOmNyZWRlbnRpYWxz")
.get();
assertThat(response, hasForbidden());
}
@Test
public void testGetIllegalAccess() throws IOException {
final Response response = target("users/" + adminLogin()).request()
.cookie("token", userToken(normalLogin()))
.get();
assertThat(response, hasUnauthorized());
}
}
......@@ -6,10 +6,12 @@ import org.junit.runners.Suite.SuiteClasses;
import es.uvigo.esei.daa.dao.PeopleDAOTest;
import es.uvigo.esei.daa.rest.PeopleResourceTest;
import es.uvigo.esei.daa.rest.UsersResourceTest;
@SuiteClasses({
PeopleDAOTest.class,
PeopleResourceTest.class
PeopleResourceTest.class,
UsersResourceTest.class
})
@RunWith(Suite.class)
public class IntegrationTestSuite {
......
......@@ -14,5 +14,6 @@
<people id="10" name="Juan" surname="Jiménez" />
<people id="11" name="John" surname="Doe" />
<users login="admin" password="0b893644f3b2097d004c58d585e784ac92dd1356d25158a298573ad54ab2d15d" />
<users login="admin" password="43f413b773f7d0cfad0e8e6529ec1249ce71e8697919eab30d82d800a3986b70" />
<users login="normal" password="688f21dd2d65970f174e2c9d35159250a8a23e27585452683db8c5d10b586336" />
</dataset>
\ No newline at end of file
......@@ -12,5 +12,6 @@
<people id="9" name="Julia" surname="Justa" />
<people id="10" name="Juan" surname="Jiménez" />
<users login="admin" password="0b893644f3b2097d004c58d585e784ac92dd1356d25158a298573ad54ab2d15d" />
<users login="admin" password="43f413b773f7d0cfad0e8e6529ec1249ce71e8697919eab30d82d800a3986b70" />
<users login="normal" password="688f21dd2d65970f174e2c9d35159250a8a23e27585452683db8c5d10b586336" />
</dataset>
\ No newline at end of file
......@@ -13,5 +13,6 @@
<people id="9" name="Julia" surname="Justa" />
<people id="10" name="Juan" surname="Jiménez" />
<users login="admin" password="0b893644f3b2097d004c58d585e784ac92dd1356d25158a298573ad54ab2d15d" />
<users login="admin" password="43f413b773f7d0cfad0e8e6529ec1249ce71e8697919eab30d82d800a3986b70" />
<users login="normal" password="688f21dd2d65970f174e2c9d35159250a8a23e27585452683db8c5d10b586336" />
</dataset>
\ No newline at end of file
......@@ -13,5 +13,6 @@
<people id="9" name="Julia" surname="Justa" />
<people id="10" name="Juan" surname="Jiménez" />
<users login="admin" password="0b893644f3b2097d004c58d585e784ac92dd1356d25158a298573ad54ab2d15d" />
<users login="admin" password="43f413b773f7d0cfad0e8e6529ec1249ce71e8697919eab30d82d800a3986b70" />
<users login="normal" password="688f21dd2d65970f174e2c9d35159250a8a23e27585452683db8c5d10b586336" />
</dataset>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment