Add user session integration. We still need to add routes that require auth in our session handler

This commit is contained in:
Colin McDonald 2016-07-12 21:56:28 -04:00
parent f42f714863
commit 2502f4a1b2
8 changed files with 204 additions and 8 deletions

View File

@ -40,10 +40,12 @@ import lombok.extern.slf4j.Slf4j;
import net.frozenorb.apiv3.handler.ActorAttributeHandler; import net.frozenorb.apiv3.handler.ActorAttributeHandler;
import net.frozenorb.apiv3.handler.AuthorizationHandler; import net.frozenorb.apiv3.handler.AuthorizationHandler;
import net.frozenorb.apiv3.handler.MetricsHandler; import net.frozenorb.apiv3.handler.MetricsHandler;
import net.frozenorb.apiv3.handler.WebsiteUserSessionHandler;
import net.frozenorb.apiv3.model.*; import net.frozenorb.apiv3.model.*;
import net.frozenorb.apiv3.route.GETDumpsType; import net.frozenorb.apiv3.route.GETDumpsType;
import net.frozenorb.apiv3.route.GETMetrics; import net.frozenorb.apiv3.route.GETMetrics;
import net.frozenorb.apiv3.route.GETWhoAmI; import net.frozenorb.apiv3.route.GETWhoAmI;
import net.frozenorb.apiv3.route.POSTLogout;
import net.frozenorb.apiv3.route.accessTokens.DELETEAccessTokensId; import net.frozenorb.apiv3.route.accessTokens.DELETEAccessTokensId;
import net.frozenorb.apiv3.route.accessTokens.GETAccessTokens; import net.frozenorb.apiv3.route.accessTokens.GETAccessTokens;
import net.frozenorb.apiv3.route.accessTokens.GETAccessTokensId; import net.frozenorb.apiv3.route.accessTokens.GETAccessTokensId;
@ -274,6 +276,7 @@ public final class APIv3 extends AbstractVerticle {
http.route().method(HttpMethod.PUT).method(HttpMethod.POST).method(HttpMethod.DELETE).handler(BodyHandler.create()); http.route().method(HttpMethod.PUT).method(HttpMethod.POST).method(HttpMethod.DELETE).handler(BodyHandler.create());
http.route().handler(new ActorAttributeHandler()); http.route().handler(new ActorAttributeHandler());
http.route().handler(new MetricsHandler()); http.route().handler(new MetricsHandler());
http.route().handler(new WebsiteUserSessionHandler());
http.route().handler(new AuthorizationHandler()); http.route().handler(new AuthorizationHandler());
http.exceptionHandler(Throwable::printStackTrace); http.exceptionHandler(Throwable::printStackTrace);
@ -376,6 +379,7 @@ public final class APIv3 extends AbstractVerticle {
http.get("/dumps/:dumpType").handler(new GETDumpsType()); http.get("/dumps/:dumpType").handler(new GETDumpsType());
http.get("/metrics").handler(new GETMetrics()); http.get("/metrics").handler(new GETMetrics());
http.get("/whoami").handler(new GETWhoAmI()); http.get("/whoami").handler(new GETWhoAmI());
http.post("/logout").handler(new POSTLogout());
int port = Integer.parseInt(config.getProperty("http.port")); int port = Integer.parseInt(config.getProperty("http.port"));
webServer.requestHandler(http::accept).listen(port); webServer.requestHandler(http::accept).listen(port);

View File

@ -2,6 +2,6 @@ package net.frozenorb.apiv3.actor;
public enum ActorType { public enum ActorType {
WEBSITE, BUNGEE_CORD, SERVER, UNKNOWN WEBSITE, STORE, BUNGEE_CORD, SERVER, UNKNOWN
} }

View File

@ -0,0 +1,71 @@
package net.frozenorb.apiv3.handler;
import com.google.common.collect.ImmutableMap;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpMethod;
import io.vertx.ext.web.RoutingContext;
import net.frozenorb.apiv3.actor.Actor;
import net.frozenorb.apiv3.actor.ActorType;
import net.frozenorb.apiv3.util.ErrorUtils;
import net.frozenorb.apiv3.util.UserSessionUtils;
public final class WebsiteUserSessionHandler implements Handler<RoutingContext> {
@Override
public void handle(RoutingContext ctx) {
Actor actor = ctx.get("actor");
if (actor.getType() != ActorType.WEBSITE) {
ctx.next();
return;
}
if (!isUserSessionRequired(ctx)) {
ctx.next();
return;
}
String userSession = ctx.request().getHeader("MHQ-UserSession");
String userIp = ctx.request().getHeader("MHQ-UserIp");
UserSessionUtils.sessionExists(userIp, userSession, (exists, error) -> {
if (error != null) {
ErrorUtils.respondInternalError(ctx, error);
return;
}
if (exists) {
ctx.next();
} else {
ErrorUtils.respondOther(ctx, 403, "User session invalid.", "userSessionInvalid", ImmutableMap.of());
}
});
}
public boolean isUserSessionRequired(RoutingContext ctx) {
HttpMethod method = ctx.request().method();
String path = ctx.request().path().toLowerCase();
/*if (method == HttpMethod.GET) {
switch (path) {
case "/grants":
case "/servers":
case "/servergroups":
case "/whoami":
return false;
}
if (path.contains("/dumps")) {
return false;
}
} else if (method == HttpMethod.POST) {
switch (path) {
case "/grants":
return false;
}
}*/
return false;
}
}

View File

@ -0,0 +1,25 @@
package net.frozenorb.apiv3.route;
import com.google.common.collect.ImmutableMap;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.util.ErrorUtils;
import net.frozenorb.apiv3.util.UserSessionUtils;
public final class POSTLogout implements Handler<RoutingContext> {
public void handle(RoutingContext ctx) {
String userSession = ctx.request().getHeader("MHQ-UserSession");
String userIp = ctx.request().getHeader("MHQ-UserIp");
UserSessionUtils.invalidateSession(userIp, userSession, (ignored, error) -> {
if (error != null) {
ErrorUtils.respondInternalError(ctx, error);
} else {
APIv3.respondJson(ctx, 200, ImmutableMap.of());
}
});
}
}

View File

@ -1,6 +1,5 @@
package net.frozenorb.apiv3.route.users; package net.frozenorb.apiv3.route.users;
import com.google.common.collect.ImmutableMap;
import io.vertx.core.Handler; import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.RoutingContext;
import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.APIv3;
@ -9,7 +8,10 @@ import net.frozenorb.apiv3.auditLog.AuditLogActionType;
import net.frozenorb.apiv3.model.User; import net.frozenorb.apiv3.model.User;
import net.frozenorb.apiv3.util.ErrorUtils; import net.frozenorb.apiv3.util.ErrorUtils;
import net.frozenorb.apiv3.util.SyncUtils; import net.frozenorb.apiv3.util.SyncUtils;
import net.frozenorb.apiv3.util.UserSessionUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public final class GETUsersIdVerifyPassword implements Handler<RoutingContext> { public final class GETUsersIdVerifyPassword implements Handler<RoutingContext> {
@ -37,15 +39,23 @@ public final class GETUsersIdVerifyPassword implements Handler<RoutingContext> {
final UUID finalUuid = user.getId(); final UUID finalUuid = user.getId();
boolean authorized = user.checkPassword(ctx.request().getParam("password")); boolean authorized = user.checkPassword(ctx.request().getParam("password"));
String userIp = ctx.request().getParam("userIp");
AuditLog.log(user.getId(), ctx.request().getParam("userIp"), ctx, authorized ? AuditLogActionType.USER_LOGIN_SUCCESS : AuditLogActionType.USER_LOGIN_FAIL, (ignored, error) -> { AuditLog.log(user.getId(), userIp, ctx, authorized ? AuditLogActionType.USER_LOGIN_SUCCESS : AuditLogActionType.USER_LOGIN_FAIL, (ignored, error) -> {
if (error != null) { if (error != null) {
ErrorUtils.respondInternalError(ctx, error); ErrorUtils.respondInternalError(ctx, error);
} else { } else {
APIv3.respondJson(ctx, 200, ImmutableMap.of( Map<String, Object> result = new HashMap<>();
"authorized", authorized,
"uuid", finalUuid result.put("authorized", authorized);
)); result.put("uuid", finalUuid);
if (authorized) {
String session = SyncUtils.runBlocking(v -> UserSessionUtils.createSession(finalUuid, userIp, v));
result.put("session", session);
}
APIv3.respondJson(ctx, 200, result);
} }
}); });
} }

View File

@ -12,6 +12,7 @@ import net.frozenorb.apiv3.unsorted.RequiresTotpResult;
import net.frozenorb.apiv3.unsorted.TotpAuthorizationResult; import net.frozenorb.apiv3.unsorted.TotpAuthorizationResult;
import net.frozenorb.apiv3.util.ErrorUtils; import net.frozenorb.apiv3.util.ErrorUtils;
import net.frozenorb.apiv3.util.SyncUtils; import net.frozenorb.apiv3.util.SyncUtils;
import net.frozenorb.apiv3.util.UserSessionUtils;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -71,6 +72,7 @@ public final class POSTUsersIdChangePassword implements Handler<RoutingContext>
user.updatePassword(newPassword); user.updatePassword(newPassword);
SyncUtils.<Void>runBlocking(v -> user.save(v)); SyncUtils.<Void>runBlocking(v -> user.save(v));
SyncUtils.<Void>runBlocking(v -> UserSessionUtils.invalidateAllSessions(user.getId(), v));
AuditLog.log(user.getId(), requestBody.getString("userIp"), ctx, AuditLogActionType.USER_CHANGE_PASSWORD, (ignored, error) -> { AuditLog.log(user.getId(), requestBody.getString("userIp"), ctx, AuditLogActionType.USER_CHANGE_PASSWORD, (ignored, error) -> {
if (error != null) { if (error != null) {

View File

@ -0,0 +1,84 @@
package net.frozenorb.apiv3.util;
import com.mongodb.async.SingleResultCallback;
import io.vertx.redis.RedisClient;
import io.vertx.redis.RedisOptions;
import lombok.experimental.UtilityClass;
import net.frozenorb.apiv3.APIv3;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@UtilityClass
public class UserSessionUtils {
private static final RedisClient redisClient = RedisClient.create(APIv3.getVertxInstance(),
new RedisOptions()
.setAddress(APIv3.getConfig().getProperty("redis.address"))
.setPort(Integer.parseInt(APIv3.getConfig().getProperty("redis.port")))
);
public static void sessionExists(String userIp, String userSession, SingleResultCallback<Boolean> callback) {
if (userIp == null || userIp.isEmpty() || userSession == null || userSession.isEmpty()) {
callback.onResult(false, null);
return;
}
redisClient.exists("apiv3:sessions:" + userIp + ":" + userSession, (result) -> {
if (result.succeeded()) {
callback.onResult(result.result() == 1, null);
} else {
callback.onResult(null, result.cause());
}
});
}
public static void createSession(UUID user, String userIp, SingleResultCallback<String> callback) {
String userSession = UUID.randomUUID().toString().replaceAll("-", "");
String key = "apiv3:sessions:" + userIp + ":" + userSession;
redisClient.setex(key, TimeUnit.DAYS.toSeconds(30), "", (result) -> {
if (result.succeeded()) {
redisClient.sadd("apiv3:sessions:" + user, key, (result2) -> {
if (result2.succeeded()) {
callback.onResult(null, null);
} else {
callback.onResult(null, result2.cause());
}
});
callback.onResult(null, null);
} else {
callback.onResult(null, result.cause());
}
});
}
public static void invalidateSession(String userIp, String userSession, SingleResultCallback<Void> callback) {
redisClient.del("apiv3:sessions:" + userIp + ":" + userSession, (result) -> {
if (result.succeeded()) {
callback.onResult(null, null);
} else {
callback.onResult(null, result.cause());
}
});
}
public static void invalidateAllSessions(UUID user, SingleResultCallback<Void> callback) {
redisClient.smembers("apiv3:sessions:" + user, (result) -> {
if (result.failed()) {
callback.onResult(null, result.cause());
return;
}
redisClient.delMany((List<String>) result.result().getList(), (result2) -> {
if (result2.succeeded()) {
callback.onResult(null, null);
} else {
callback.onResult(null, result2.cause());
}
});
});
}
}