From 441c660af706fd3c6d0e06b36b8f25a808fcdf5f Mon Sep 17 00:00:00 2001
From: Roberto Sánchez <roberto.sanchez@curisit.net>
Date: Fri, 17 Jan 2014 17:35:50 +0000
Subject: [PATCH] #396 feature - Added security management methods for REST API

---
 securis/src/main/java/net/curisit/securis/services/SecurityInterceptor.java |   64 ++++++++--
 securis/src/main/java/net/curisit/securis/utils/TokenHelper.java            |   22 +++
 securis/src/main/java/net/curisit/securis/services/UserResource.java        |   32 +++++
 securis/src/main/java/net/curisit/securis/utils/CacheTTL.java               |  125 ++++++++++++++++++++
 securis/src/main/java/net/curisit/securis/services/Securable.java           |   12 +
 securis/src/main/java/net/curisit/securis/services/BasicServices.java       |   70 +++++++++++
 securis/src/main/resources/static/js/main.js                                |   17 ++
 7 files changed, 320 insertions(+), 22 deletions(-)

diff --git a/securis/src/main/java/net/curisit/securis/services/BasicServices.java b/securis/src/main/java/net/curisit/securis/services/BasicServices.java
index 38addaa..ef66c32 100644
--- a/securis/src/main/java/net/curisit/securis/services/BasicServices.java
+++ b/securis/src/main/java/net/curisit/securis/services/BasicServices.java
@@ -6,14 +6,23 @@
 import javax.inject.Inject;
 import javax.inject.Singleton;
 import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.FormParam;
 import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.UriBuilder;
+
+import net.curisit.integrity.commons.Utils;
+import net.curisit.securis.db.User;
+import net.curisit.securis.utils.TokenHelper;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -28,6 +37,9 @@
 public class BasicServices {
 
 	private static final Logger log = LoggerFactory.getLogger(BasicServices.class);
+
+	@Inject
+	TokenHelper tokenHelper;
 
 	@Inject
 	public BasicServices() {
@@ -52,4 +64,62 @@
 		return Response.seeOther(uri).build();
 	}
 
+	@POST
+	@Path("/login")
+	@Produces(
+		{ MediaType.APPLICATION_JSON })
+	public Response login(@FormParam("username") String user, @FormParam("password") String password, @Context HttpServletRequest request) {
+		log.info("index session: " + request.getSession());
+		log.info("user: {}, pass: {}", user, password);
+		log.info("is user in role: {} == {} ? ", "advance", request.isUserInRole("advance"));
+
+		if ("no".equals(password))
+			return Response.status(Status.UNAUTHORIZED).build();
+		String tokenAuth = tokenHelper.generateToken(user);
+		return Response.ok(Utils.createMap("success", true, "token", tokenAuth)).build();
+	}
+
+	/**
+	 * Check if current token is valid
+	 * 
+	 * @param user
+	 * @param password
+	 * @param request
+	 * @return
+	 */
+	@GET
+	@Securable(roles = User.Rol.ADMIN)
+	@Path("/check")
+	@Produces(
+		{ MediaType.APPLICATION_JSON })
+	public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
+		if (token == null)
+			token = token2;
+		if (token == null)
+			return Response.status(Status.FORBIDDEN).build();
+		boolean valid = tokenHelper.isTokenValid(token);
+		if (!valid)
+			return Response.status(Status.UNAUTHORIZED).build();
+
+		// log.info("Token : " + token);
+		String user = tokenHelper.extractUserFromToken(token);
+		// log.info("Token user: " + user);
+		Date date = tokenHelper.extractDateCreationFromToken(token);
+		// log.info("Token date: " + date);
+
+		return Response.ok(Utils.createMap("valid", true, "user", user, "date", date)).build();
+	}
+
+	@GET
+	@POST
+	@Path("/logout")
+	@Produces(
+		{ MediaType.APPLICATION_JSON })
+	public Response logout(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+		if (token == null)
+			Response.status(Status.BAD_REQUEST).build();
+		String user = tokenHelper.extractUserFromToken(token);
+		log.info("User {} has logged out", user);
+		return Response.ok().build();
+	}
 }
diff --git a/securis/src/main/java/net/curisit/securis/services/Securable.java b/securis/src/main/java/net/curisit/securis/services/Securable.java
index cad8f57..6fcb8e3 100644
--- a/securis/src/main/java/net/curisit/securis/services/Securable.java
+++ b/securis/src/main/java/net/curisit/securis/services/Securable.java
@@ -5,8 +5,18 @@
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
+import net.curisit.securis.utils.TokenHelper;
+
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.METHOD)
 public @interface Securable {
-	String header() default "session-token";
+	/**
+	 * Name of header parameter with the auth token to validate
+	 */
+	String header() default TokenHelper.TOKEN_HEADER_PÀRAM;
+
+	/**
+	 * Bit mask with the rol or roles necessary to access the method
+	 */
+	int roles() default 0;
 }
\ No newline at end of file
diff --git a/securis/src/main/java/net/curisit/securis/services/SecurityInterceptor.java b/securis/src/main/java/net/curisit/securis/services/SecurityInterceptor.java
index 16e2444..0516435 100644
--- a/securis/src/main/java/net/curisit/securis/services/SecurityInterceptor.java
+++ b/securis/src/main/java/net/curisit/securis/services/SecurityInterceptor.java
@@ -2,11 +2,20 @@
 
 import java.io.IOException;
 import java.lang.reflect.Method;
+import java.util.List;
 
+import javax.inject.Inject;
+import javax.persistence.EntityManager;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.container.ContainerRequestContext;
 import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.ext.Provider;
+
+import net.curisit.securis.db.User;
+import net.curisit.securis.utils.CacheTTL;
+import net.curisit.securis.utils.TokenHelper;
 
 import org.jboss.resteasy.core.ResourceMethodInvoker;
 import org.slf4j.Logger;
@@ -17,33 +26,60 @@
 
 	private static final Logger log = LoggerFactory.getLogger(SecurityInterceptor.class);
 
+	@Inject
+	private TokenHelper tokenHelper;
+
 	@Context
 	private HttpServletRequest servletRequest;
+
+	@Inject
+	CacheTTL cache;
+
+	@Inject
+	com.google.inject.Provider<EntityManager> emProvider;
 
 	@Override
 	public void filter(ContainerRequestContext containerRequestContext) throws IOException {
 		log.info("filter using REST interceptor, method: {}", containerRequestContext.getMethod());
+
 		log.info("filter using REST interceptor, ResourceMethodInvoker: {}", containerRequestContext.getProperty("org.jboss.resteasy.core.ResourceMethodInvoker"));
 		ResourceMethodInvoker methodInvoker = (ResourceMethodInvoker) containerRequestContext.getProperty("org.jboss.resteasy.core.ResourceMethodInvoker");
 		Method method = methodInvoker.getMethod();
 
 		if (!method.isAnnotationPresent(Securable.class))
 			return;
+		String token = servletRequest.getHeader(TokenHelper.TOKEN_HEADER_PÀRAM);
+		if (token == null || !tokenHelper.isTokenValid(token))
+			containerRequestContext.abortWith(Response.status(Status.UNAUTHORIZED).build());
+		Securable sec = method.getAnnotation(Securable.class);
+
+		// If roles == 0 we only need to validate the token
+		if (sec.roles() != 0) {
+			String username = tokenHelper.extractUserFromToken(token);
+			int userRoles = getUserRoles(username);
+			if ((sec.roles() & userRoles) == 0) {
+				log.info("User {} has no necessary role to access url: {}", username, servletRequest.getPathInfo());
+				containerRequestContext.abortWith(Response.status(Status.UNAUTHORIZED).build());
+			}
+		}
 	}
 
-	// @Override
-	// public ServerResponse preProcess(HttpRequest httpRequest, ResourceMethod resourceMethod) throws Failure, WebApplicationException {
-	//
-	// Securable securable = resourceMethod.getMethod().getAnnotation(Securable.class);
-	// String headerValue = servletRequest.getHeader(securable.header());
-	//
-	// if (headerValue == null) {
-	// return (ServerResponse) Response.status(Status.BAD_REQUEST).entity("Invalid Session").build();
-	// } else {
-	// // Validatation logic goes here
-	// }
-	//
-	// return null;
-	// }
+	private int getUserRoles(String username) {
+		Integer userRoles = cache.get("roles_" + username, Integer.class);
+		if (userRoles == null) {
+			EntityManager em = emProvider.get();
+			User user = em.find(User.class, username);
+			if (user != null) {
+				userRoles = 0;
+				List<Integer> roles = user.getRoles();
+				for (Integer rol : roles) {
+					userRoles += rol;
+				}
+				// We store user roles in cache only for one hour
+				cache.set("roles_" + username, userRoles, 3600);
+			}
+		}
+		return userRoles == null ? 0 : userRoles.intValue();
+	}
 
 }
diff --git a/securis/src/main/java/net/curisit/securis/services/UserResource.java b/securis/src/main/java/net/curisit/securis/services/UserResource.java
index 0b5e208..2bc90ee 100644
--- a/securis/src/main/java/net/curisit/securis/services/UserResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/UserResource.java
@@ -19,6 +19,7 @@
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
@@ -206,13 +207,42 @@
 		log.info("user: {}, pass: {}", user, password);
 		log.info("is user in role: {} == {} ? ", "advance", request.isUserInRole("advance"));
 
-		request.getSession().setAttribute("username", user);
 		if ("no".equals(password))
 			return Response.status(Status.UNAUTHORIZED).build();
 		String tokenAuth = tokenHelper.generateToken(user);
 		return Response.ok(Utils.createMap("success", true, "token", tokenAuth)).build();
 	}
 
+	/**
+	 * Check if current token is valid
+	 * 
+	 * @param user
+	 * @param password
+	 * @param request
+	 * @return
+	 */
+	@POST
+	@Path("/check")
+	@Produces(
+		{ MediaType.APPLICATION_JSON })
+	public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
+		if (token == null)
+			token = token2;
+		if (token == null)
+			return Response.status(Status.FORBIDDEN).build();
+
+		log.info("Token : " + token);
+		String user = tokenHelper.extractUserFromToken(token);
+		log.info("Token user: " + user);
+		Date date = tokenHelper.extractDateCreationFromToken(token);
+		log.info("Token date: " + date);
+		boolean valid = tokenHelper.isTokenValid(token);
+
+		log.info("Is Token valid: " + valid);
+
+		return Response.ok(Utils.createMap("valid", true, "user", user, "date", date, "token", token)).build();
+	}
+
 	@GET
 	@Path("/logout")
 	@Produces(
diff --git a/securis/src/main/java/net/curisit/securis/utils/CacheTTL.java b/securis/src/main/java/net/curisit/securis/utils/CacheTTL.java
new file mode 100644
index 0000000..51a59e4
--- /dev/null
+++ b/securis/src/main/java/net/curisit/securis/utils/CacheTTL.java
@@ -0,0 +1,125 @@
+package net.curisit.securis.utils;
+
+import java.util.Date;
+import java.util.Hashtable;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Cache implementation with TTL (time To Live) The objects are removed from cache when TTL is reached.
+ * 
+ * @author roberto <roberto.sanchez@curisit.net>
+ */
+@Singleton
+public class CacheTTL {
+
+	private static final Logger log = LoggerFactory.getLogger(CacheTTL.class);
+
+	/**
+	 * Period before token expires, set in seconds.
+	 */
+	private static int DEFAULT_CACHE_DURATION = 24 * 60 * 60;
+
+	private Map<String, CachedObject> data = new Hashtable<>();
+
+	private Thread cleaningThread = null;
+
+	@Inject
+	public CacheTTL() {
+		cleaningThread = new Thread(new Runnable() {
+
+			@Override
+			public void run() {
+				while (CacheTTL.this.data != null) {
+					try {
+						// We check for expired object every 60 seconds
+						Thread.sleep(60 * 1000);
+					} catch (InterruptedException e) {
+						log.error("Exiting from Cache Thread");
+						data.clear();
+						return;
+					}
+					// log.info("Cheking expired objects " + new Date());
+					Date now = new Date();
+					for (String key : CacheTTL.this.data.keySet()) {
+						CachedObject co = CacheTTL.this.data.get(key);
+						if (now.after(co.getExpireAt())) {
+							CacheTTL.this.data.remove(key);
+						}
+					}
+				}
+			}
+		});
+		cleaningThread.start();
+	}
+
+	/**
+	 * 
+	 * @param key
+	 * @param obj
+	 * @param ttl
+	 *            Time To Live in seconds
+	 */
+	public void set(String key, Object obj, int ttl) {
+		Date expirationDate = new Date(new Date().getTime() + ttl * 1000);
+		data.put(key, new CachedObject(expirationDate, obj));
+	}
+
+	public void set(String key, Object obj) {
+		set(key, obj, DEFAULT_CACHE_DURATION);
+	}
+
+	public Object get(String key) {
+		CachedObject co = data.get(key);
+		return co == null ? null : co.getObject();
+	}
+
+	public <T> T get(String key, Class<T> type) {
+		CachedObject co = data.get(key);
+		return co == null ? null : co.getObject(type);
+	}
+
+	public <T> T remove(String key, Class<T> type) {
+		CachedObject co = data.remove(key);
+		return co == null ? null : co.getObject(type);
+	}
+
+	public Object remove(String key) {
+		CachedObject co = data.remove(key);
+		return co == null ? null : co.getObject();
+	}
+
+	public void clear() {
+		data.clear();
+	}
+
+	private class CachedObject {
+		Date expireAt;
+		Object object;
+
+		public CachedObject(Date date, Object obj) {
+			expireAt = date;
+			object = obj;
+		}
+
+		public Date getExpireAt() {
+			return expireAt;
+		}
+
+		public Object getObject() {
+			return object;
+		}
+
+		@SuppressWarnings("unchecked")
+		public <T> T getObject(Class<T> type) {
+			return (T) object;
+		}
+
+	}
+
+}
diff --git a/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java b/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
index df247d0..b7d82de 100644
--- a/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
+++ b/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
@@ -78,10 +78,12 @@
 	 * @param token
 	 * @return
 	 */
-	public boolean validateToken(String token) {
+	public boolean isTokenValid(String token) {
 		try {
 			String tokenDecoded = new String(Base64.decode(token));
 			String[] parts = StringUtils.split(tokenDecoded, ' ');
+			if (parts == null || parts.length < 3)
+				return false;
 			String secret = parts[0];
 			String user = parts[1];
 			Date date = Utils.toDateFromIso(parts[2]);
@@ -101,8 +103,24 @@
 		try {
 			String tokenDecoded = new String(Base64.decode(token));
 			String[] parts = StringUtils.split(tokenDecoded, ' ');
+			if (parts == null || parts.length < 3)
+				return null;
 			String user = parts[1];
 			return user;
+		} catch (IOException e) {
+			log.error("Error decoding Base64 token", e);
+		}
+		return null;
+	}
+
+	public Date extractDateCreationFromToken(String token) {
+		try {
+			String tokenDecoded = new String(Base64.decode(token));
+			String[] parts = StringUtils.split(tokenDecoded, ' ');
+			if (parts == null || parts.length < 3)
+				return null;
+			Date date = Utils.toDateFromIso(parts[2]);
+			return date;
 		} catch (IOException e) {
 			log.error("Error decoding Base64 token", e);
 		}
@@ -114,6 +132,6 @@
 		String token = th.generateToken("pepe");
 		System.out.println("Token: " + token);
 		System.out.println("Token: " + new String(Base64.decode(token)));
-		System.out.println("Valid Token: " + th.validateToken(token));
+		System.out.println("Valid Token: " + th.isTokenValid(token));
 	}
 }
diff --git a/securis/src/main/resources/static/js/main.js b/securis/src/main/resources/static/js/main.js
index 14521b6..58a7a9b 100644
--- a/securis/src/main/resources/static/js/main.js
+++ b/securis/src/main/resources/static/js/main.js
@@ -67,11 +67,20 @@
 	m.controller('MainCtrl', ['$scope', '$http', '$location', '$L', '$store',
 	                             function($scope, $http, $location, $L, $store) {
 		
+		$location.path('/login');
 		if ($store.get('token') != null) {
-			$http.defaults.headers.common['X-SECURIS-TOKEN'] = $store.get('token'); 
-			$location.path('/licenses');
-		} else {
-			$location.path('/login');
+			
+			$http.get('/check', {
+				headers: {
+					'X-SECURIS-TOKEN': $store.get('token')
+				}
+			}).success(function(data) {
+				if (data.valid) {
+					$http.defaults.headers.common['X-SECURIS-TOKEN'] = $store.get('token'); 
+					$location.path('/licenses');
+					$store.set('user', data.user);
+				}
+			});
 		}
 		
 		$scope.logout = function() {

--
Gitblit v1.3.2