Convert authorization to use access tokens. Completes #24

This commit is contained in:
Colin McDonald 2016-06-27 16:55:21 -04:00
parent bcae144e8c
commit 29a13c1647
21 changed files with 179 additions and 287 deletions

View File

@ -11,5 +11,3 @@ maxMind.userId=66817
maxMind.maxMindLicenseKey=8Aw9NsOUeOp7
zang.accountSid=ACf18890845596403e330944d98886440c
zang.authToken=dc70bbd1fbd8411ba133fa93813a461b
auth.websiteApiKey=RVbp4hY6sCFVaf
auth.bungeeApiKey=6d9cf76dc9f0d23

View File

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

View File

@ -0,0 +1,13 @@
package net.frozenorb.apiv3.actor;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
public final class SimpleActor implements Actor {
@Getter private String name;
@Getter private ActorType type;
@Getter private boolean authorized;
}

View File

@ -1,23 +0,0 @@
package net.frozenorb.apiv3.actor.actors;
import net.frozenorb.apiv3.actor.Actor;
import net.frozenorb.apiv3.actor.ActorType;
public final class BungeeActor implements Actor {
@Override
public boolean isAuthorized() {
return true;
}
@Override
public String getName() {
return "Bungee";
}
@Override
public ActorType getType() {
return ActorType.BUNGEE;
}
}

View File

@ -1,31 +0,0 @@
package net.frozenorb.apiv3.actor.actors;
import lombok.Getter;
import net.frozenorb.apiv3.actor.Actor;
import net.frozenorb.apiv3.actor.ActorType;
import net.frozenorb.apiv3.model.Server;
public final class ServerActor implements Actor {
@Getter private final Server server;
public ServerActor(Server server) {
this.server = server;
}
@Override
public boolean isAuthorized() {
return true;
}
@Override
public String getName() {
return server.getId();
}
@Override
public ActorType getType() {
return ActorType.SERVER;
}
}

View File

@ -1,23 +0,0 @@
package net.frozenorb.apiv3.actor.actors;
import net.frozenorb.apiv3.actor.Actor;
import net.frozenorb.apiv3.actor.ActorType;
public final class UnknownActor implements Actor {
@Override
public boolean isAuthorized() {
return false;
}
@Override
public String getName() {
return "Unknown";
}
@Override
public ActorType getType() {
return ActorType.UNKNOWN;
}
}

View File

@ -1,33 +0,0 @@
package net.frozenorb.apiv3.actor.actors;
import lombok.Getter;
import net.frozenorb.apiv3.actor.Actor;
import net.frozenorb.apiv3.actor.ActorType;
import net.frozenorb.apiv3.model.User;
public final class UserActor implements Actor {
@Getter private final User user;
private final boolean authorized;
public UserActor(User user, boolean authorized) {
this.user = user;
this.authorized = authorized;
}
@Override
public boolean isAuthorized() {
return authorized;
}
@Override
public String getName() {
return user.getLastUsername();
}
@Override
public ActorType getType() {
return ActorType.USER;
}
}

View File

@ -1,23 +0,0 @@
package net.frozenorb.apiv3.actor.actors;
import net.frozenorb.apiv3.actor.Actor;
import net.frozenorb.apiv3.actor.ActorType;
public final class WebsiteActor implements Actor {
@Override
public boolean isAuthorized() {
return true;
}
@Override
public String getName() {
return "Website";
}
@Override
public ActorType getType() {
return ActorType.WEBSITE;
}
}

View File

@ -46,7 +46,7 @@ public final class PunishmentConverter implements Block<Document> {
null,
punishment.containsKey("addedBy") ? oidToUniqueId.get(((Map<String, Object>) punishment.get("addedBy")).get("$id")) : null,
punishment.getDate("created").toInstant(),
punishment.containsKey("createdOn") ? String.valueOf(((Map<String, Object>) punishment.get("createdOn")).get("$id")) : "Website",
punishment.containsKey("createdOn") ? String.valueOf(((Map<String, Object>) punishment.get("createdOn")).get("$id")) : "Old Website",
punishment.containsKey("createdOn") ? ActorType.SERVER : ActorType.WEBSITE,
punishment.containsKey("removedBy") ? (((Map<String, Object>) punishment.get("removedBy")).get("$ref").equals("user") ? oidToUniqueId.get(((Map<String, Object>) punishment.get("removedBy")).get("$id")) : null) : null,
punishment.containsKey("removedBy") ? (punishment.containsKey("removedAt") ? punishment.getDate("removedAt") : punishment.getDate("created")).toInstant() : null,

View File

@ -1,133 +1,58 @@
package net.frozenorb.apiv3.handler;
import com.google.common.base.Charsets;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.actor.actors.*;
import net.frozenorb.apiv3.model.Server;
import net.frozenorb.apiv3.model.User;
import net.frozenorb.apiv3.unsorted.Permissions;
import net.frozenorb.apiv3.actor.ActorType;
import net.frozenorb.apiv3.actor.SimpleActor;
import net.frozenorb.apiv3.model.AccessToken;
import net.frozenorb.apiv3.util.ErrorUtils;
import java.util.Base64;
public final class ActorAttributeHandler implements Handler<RoutingContext> {
@Override
public void handle(RoutingContext ctx) {
String authorizationHeader = ctx.request().getHeader("Authorization");
String mhqAuthorizationHeader = ctx.request().getHeader("MHQ-Authorization");
if (authorizationHeader != null) {
processBasicAuthorization(authorizationHeader, ctx);
} else if (mhqAuthorizationHeader != null) {
if (mhqAuthorizationHeader != null) {
processMHQAuthorization(mhqAuthorizationHeader, ctx);
} else {
processNoAuthorization(ctx);
}
}
private void processBasicAuthorization(String authHeader, RoutingContext ctx) {
String encodedHeader = authHeader.substring("Basic ".length());
String decodedHeader = new String(Base64.getDecoder().decode(encodedHeader.getBytes(Charsets.UTF_8)), Charsets.UTF_8);
String[] splitHeader = decodedHeader.split(":");
if (splitHeader.length != 2) {
ErrorUtils.respondGeneric(ctx, 401, "Failed to authorize.");
private void processMHQAuthorization(String accessTokenString, RoutingContext ctx) {
if (accessTokenString == null || accessTokenString.isEmpty()) {
ErrorUtils.respondGeneric(ctx, 401, "Failed to authorize: Key not provided");
return;
}
String username = splitHeader[0];
String password = splitHeader[1];
User.findByLastUsername(username, (user, error) -> {
AccessToken.findById(accessTokenString, (accessToken, error) -> {
if (error != null) {
ErrorUtils.respondInternalError(ctx, error);
return;
}
if (user != null && user.checkPassword(password)) {
user.hasPermissionAnywhere(Permissions.SIGN_API_REQUEST, (hasPermission, error2) -> {
if (error2 != null) {
ErrorUtils.respondInternalError(ctx, error2);
} else {
ctx.put("actor", new UserActor(user, hasPermission));
ctx.next();
}
});
} else {
ErrorUtils.respondGeneric(ctx, 401, "Failed to authorize as " + username + ".");
}
});
}
private void processMHQAuthorization(String authHeader, RoutingContext ctx) {
String[] splitHeader = authHeader.split(" ");
if (splitHeader.length < 2) {
if (accessToken == null) {
ErrorUtils.respondGeneric(ctx, 401, "Failed to authorize.");
return;
}
String type = splitHeader[0];
if (!accessToken.getLockedIps().isEmpty()) {
boolean allowed = accessToken.getLockedIps().contains(ctx.request().remoteAddress().host());
switch (type.toLowerCase()) {
case "website":
String givenWebsiteKey = splitHeader[1];
String properWebsiteKey = APIv3.getConfig().getProperty("auth.websiteApiKey");
if (givenWebsiteKey.equals(properWebsiteKey)) {
ctx.put("actor", new WebsiteActor());
ctx.next();
} else {
ErrorUtils.respondGeneric(ctx, 401, "Failed to authorize as website.");
}
break;
case "server":
Server server = Server.findById(splitHeader[1]);
if (server == null) {
ErrorUtils.respondGeneric(ctx, 401, "Failed to authorize: Server " + splitHeader[1] + " not found");
if (!allowed) {
ErrorUtils.respondGeneric(ctx, 401, "Your ip address is not authenticated to use the provided access token.");
return;
}
if (splitHeader.length != 3) {
ErrorUtils.respondGeneric(ctx, 401, "Failed to authorize: Key not provided");
return;
}
String givenServerKey = splitHeader[2];
if (givenServerKey.equals(server.getApiKey())) {
ctx.put("actor", new ServerActor(server));
ctx.put("actor", new SimpleActor(accessToken.getActorName(), accessToken.getActorType(), true));
ctx.next();
} else {
ErrorUtils.respondGeneric(ctx, 401, "Failed to authorize as " + server.getId() + ".");
}
break;
case "bungee":
String givenBungeeKey = splitHeader[1];
String properBungeeKey = APIv3.getConfig().getProperty("auth.bungeeApiKey");
if (givenBungeeKey.equals(properBungeeKey)) {
ctx.put("actor", new BungeeActor());
ctx.next();
} else {
ErrorUtils.respondGeneric(ctx, 401, "Failed to authorize as bungee.");
}
break;
default:
ErrorUtils.respondGeneric(ctx, 401, "Failed to authorize as " + type + ".");
break;
}
});
}
private void processNoAuthorization(RoutingContext ctx) {
ctx.put("actor", new UnknownActor());
ctx.put("actor", new SimpleActor("UNKNOWN", ActorType.UNKNOWN, false));
ctx.next();
}

View File

@ -3,9 +3,6 @@ package net.frozenorb.apiv3.handler;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;
import net.frozenorb.apiv3.actor.Actor;
import net.frozenorb.apiv3.actor.ActorType;
import net.frozenorb.apiv3.actor.actors.ServerActor;
import net.frozenorb.apiv3.model.Server;
import net.frozenorb.apiv3.util.ErrorUtils;
public final class AuthorizationHandler implements Handler<RoutingContext> {
@ -14,24 +11,11 @@ public final class AuthorizationHandler implements Handler<RoutingContext> {
public void handle(RoutingContext ctx) {
Actor actor = ctx.get("actor");
if (!actor.isAuthorized()) {
ErrorUtils.respondGeneric(ctx, 403, "Please authorize as an approved actor. You're currently authorized as " + actor.getName() + " (" + actor.getType() + ")");
return;
}
if (actor.getType() == ActorType.SERVER) {
Server server = ((ServerActor) actor).getServer();
String[] serverAddress = server.getServerIp().split(":");
String expectedHost = serverAddress[0];
String remoteHost = ctx.request().remoteAddress().host();
if (!expectedHost.equals(remoteHost)) {
ErrorUtils.respondGeneric(ctx, 403, "Failed to authorize: Cannot authorize as " + server.getId() + " from given ip.");
return;
}
}
if (actor.isAuthorized()) {
ctx.next();
} else {
ErrorUtils.respondGeneric(ctx, 403, "Please authorize as an approved actor. You're currently authorized as " + actor.getName() + " (" + actor.getType() + ")");
}
}
}

View File

@ -0,0 +1,77 @@
package net.frozenorb.apiv3.model;
import com.google.common.collect.ImmutableList;
import com.mongodb.async.SingleResultCallback;
import com.mongodb.async.client.MongoCollection;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import fr.javatic.mongo.jacksonCodec.Entity;
import fr.javatic.mongo.jacksonCodec.objectId.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.actor.Actor;
import net.frozenorb.apiv3.actor.ActorType;
import net.frozenorb.apiv3.maxmind.MaxMindResult;
import net.frozenorb.apiv3.util.MaxMindUtils;
import org.bson.Document;
import java.time.Instant;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@Entity
@AllArgsConstructor
public final class AccessToken {
private static final MongoCollection<AccessToken> accessTokensCollection = APIv3.getDatabase().getCollection("accessTokens", AccessToken.class);
@Getter @Id private String id;
@Getter private String actorName;
@Getter private ActorType actorType;
@Getter private List<String> lockedIps;
@Getter private Instant createdAt;
@Getter private Instant lastUpdatedAt;
public static void findAll(SingleResultCallback<List<AccessToken>> callback) {
accessTokensCollection.find().into(new LinkedList<>(), callback);
}
public static void findById(String id, SingleResultCallback<AccessToken> callback) {
accessTokensCollection.find(new Document("_id", id)).first(callback);
}
public static void findByNameAndType(String actorName, ActorType actorType, SingleResultCallback<AccessToken> callback) {
accessTokensCollection.find(new Document("actorName", actorName).append("actorType", actorType.name())).first(callback);
}
private AccessToken() {} // For Jackson
public AccessToken(Server server) {
// Can't extract server host code to another line because the call to another constructor must be on the first line.
this(UUID.randomUUID().toString().replace("-", ""), server.getId(), ActorType.SERVER, ImmutableList.of(server.getServerIp().split(":")[0]));
}
public AccessToken(String id, String actorName, ActorType actorType, List<String> lockedIps) {
this.id = id;
this.actorName = actorName;
this.actorType = actorType;
this.lockedIps = lockedIps;
this.createdAt = Instant.now();
this.lastUpdatedAt = Instant.now();
}
public void insert(SingleResultCallback<Void> callback) {
accessTokensCollection.insertOne(this, callback);
}
public void save(SingleResultCallback<UpdateResult> callback) {
accessTokensCollection.replaceOne(new Document("_id", id), this, callback);
}
public void delete(SingleResultCallback<DeleteResult> callback) {
accessTokensCollection.deleteOne(new Document("_id", id), callback);
}
}

View File

@ -27,7 +27,6 @@ public final class Server {
@Getter @Id private String id;
@Getter private String displayName;
@Getter @ExcludeFromReplies String apiKey;
@Getter private String serverGroup;
@Getter private String serverIp;
@Getter private Instant lastUpdatedAt;
@ -103,10 +102,9 @@ public final class Server {
private Server() {} // For Jackson
public Server(String id, String displayName, String apiKey, ServerGroup serverGroup, String serverIp) {
public Server(String id, String displayName, ServerGroup serverGroup, String serverIp) {
this.id = id;
this.displayName = displayName;
this.apiKey = apiKey;
this.serverGroup = serverGroup.getId();
this.serverIp = serverIp;
this.lastUpdatedAt = Instant.now();

View File

@ -6,8 +6,10 @@ import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.actor.ActorType;
import net.frozenorb.apiv3.auditLog.AuditLog;
import net.frozenorb.apiv3.auditLog.AuditLogActionType;
import net.frozenorb.apiv3.model.AccessToken;
import net.frozenorb.apiv3.model.Server;
import net.frozenorb.apiv3.unsorted.BlockingCallback;
import net.frozenorb.apiv3.util.ErrorUtils;
@ -24,9 +26,21 @@ public final class DELETEServersId implements Handler<RoutingContext> {
return;
}
BlockingCallback<DeleteResult> callback = new BlockingCallback<>();
server.delete(callback);
callback.get();
BlockingCallback<DeleteResult> deleteServerCallback = new BlockingCallback<>();
server.delete(deleteServerCallback);
deleteServerCallback.get();
BlockingCallback<DeleteResult> deleteAccessTokenCallback = new BlockingCallback<>();
AccessToken.findByNameAndType(server.getId(), ActorType.SERVER, (accessToken, error) -> {
if (error != null) {
deleteAccessTokenCallback.onResult(null, error);
} else if (accessToken != null) {
accessToken.delete(deleteServerCallback);
} else {
deleteAccessTokenCallback.onResult(null, new NullPointerException("Access token not found."));
}
});
deleteAccessTokenCallback.get();
JsonObject requestBody = ctx.getBodyAsJson();

View File

@ -7,6 +7,7 @@ import io.vertx.ext.web.RoutingContext;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.auditLog.AuditLog;
import net.frozenorb.apiv3.auditLog.AuditLogActionType;
import net.frozenorb.apiv3.model.AccessToken;
import net.frozenorb.apiv3.model.Server;
import net.frozenorb.apiv3.model.ServerGroup;
import net.frozenorb.apiv3.unsorted.BlockingCallback;
@ -34,11 +35,15 @@ public final class POSTServers implements Handler<RoutingContext> {
return;
}
String generatedApiKey = UUID.randomUUID().toString();
Server server = new Server(id, displayName, generatedApiKey, group, ip);
BlockingCallback<Void> callback = new BlockingCallback<>();
server.insert(callback);
callback.get();
Server server = new Server(id, displayName, group, ip);
BlockingCallback<Void> insertServerCallback = new BlockingCallback<>();
server.insert(insertServerCallback);
insertServerCallback.get();
AccessToken accessToken = new AccessToken(server);
BlockingCallback<Void> insertAccessTokenCallback = new BlockingCallback<>();
accessToken.insert(insertAccessTokenCallback);
insertAccessTokenCallback.get();
if (requestBody.containsKey("addedBy")) {
AuditLog.log(UUID.fromString(requestBody.getString("addedBy")), requestBody.getString("addedByIp"), ctx, AuditLogActionType.SERVER_CREATE, ImmutableMap.of("serverId", id), (ignored, error) -> {

View File

@ -12,7 +12,6 @@ import lombok.extern.slf4j.Slf4j;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.actor.Actor;
import net.frozenorb.apiv3.actor.ActorType;
import net.frozenorb.apiv3.actor.actors.ServerActor;
import net.frozenorb.apiv3.model.*;
import net.frozenorb.apiv3.unsorted.FutureCompatibilityCallback;
import net.frozenorb.apiv3.util.ErrorUtils;
@ -35,7 +34,7 @@ public final class POSTServersHeartbeat implements Handler<RoutingContext> {
return;
}
Server actorServer = ((ServerActor) actor).getServer();
Server actorServer = Server.findById(actor.getName());
ServerGroup actorServerGroup = ServerGroup.findById(actorServer.getServerGroup());
JsonObject requestBody = ctx.getBodyAsJson();
Map<UUID, String> playerNames = extractPlayerNames(requestBody.getJsonObject("players"));
@ -132,7 +131,7 @@ public final class POSTServersHeartbeat implements Handler<RoutingContext> {
serverGroup.calculateScopedPermissions(rank)
);
permissionsResponse.put(rank.getId(), scopedPermissions);
permissionsResponse.put(rank.getId(), PermissionUtils.convertToList(scopedPermissions));
}
callback.complete(permissionsResponse);

View File

@ -5,6 +5,7 @@ import io.vertx.ext.web.RoutingContext;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.model.User;
import net.frozenorb.apiv3.util.ErrorUtils;
import net.frozenorb.apiv3.util.PermissionUtils;
public final class GETUsersIdCompoundedPermissions implements Handler<RoutingContext> {
@ -19,7 +20,7 @@ public final class GETUsersIdCompoundedPermissions implements Handler<RoutingCon
if (error2 != null) {
ErrorUtils.respondInternalError(ctx, error2);
} else {
APIv3.respondJson(ctx, permissions);
APIv3.respondJson(ctx, PermissionUtils.convertToList(permissions));
}
});
}

View File

@ -5,7 +5,6 @@ import io.vertx.ext.web.RoutingContext;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.actor.Actor;
import net.frozenorb.apiv3.actor.ActorType;
import net.frozenorb.apiv3.actor.actors.ServerActor;
import net.frozenorb.apiv3.model.Server;
import net.frozenorb.apiv3.model.User;
import net.frozenorb.apiv3.util.ErrorUtils;
@ -21,7 +20,7 @@ public class POSTUsersIdLeave implements Handler<RoutingContext> {
return;
}
Server actorServer = ((ServerActor) actor).getServer();
Server actorServer = Server.findById(actor.getName());
User.findById(ctx.request().getParam("id"), ((user, error) -> {
if (error != null) {

View File

@ -7,7 +7,6 @@ import io.vertx.ext.web.RoutingContext;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.actor.Actor;
import net.frozenorb.apiv3.actor.ActorType;
import net.frozenorb.apiv3.actor.actors.ServerActor;
import net.frozenorb.apiv3.model.IpLogEntry;
import net.frozenorb.apiv3.model.Server;
import net.frozenorb.apiv3.model.User;
@ -41,7 +40,7 @@ public final class POSTUsersIdLogin implements Handler<RoutingContext> {
return;
}
Server actorServer = ((ServerActor) actor).getServer();
Server actorServer = Server.findById(actor.getName());
if (!IpUtils.isValidIp(userIp)) {
ErrorUtils.respondInvalidInput(ctx, "IP address \"" + userIp + "\" is not valid.");

View File

@ -6,7 +6,6 @@ import lombok.experimental.UtilityClass;
public class Permissions {
public static final String PROTECTED_PUNISHMENT = "minehq.punishment.protected";
public static final String SIGN_API_REQUEST = "apiv3.signRequest";
public static final String BYPASS_VPN_CHECK = "minehq.vpn.bypass";
}

View File

@ -1,6 +1,8 @@
package net.frozenorb.apiv3.util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.vertx.core.cli.converters.BooleanConverter;
import lombok.experimental.UtilityClass;
import net.frozenorb.apiv3.model.Rank;
@ -31,7 +33,7 @@ public class PermissionUtils {
}
for (Rank rank : mergeQueue) {
Map<String, Boolean> rankPermissions = convertToMap(raw.get(rank.getId()));
Map<String, Boolean> rankPermissions = convertFromList(raw.get(rank.getId()));
// If there's no permissions defined for this rank just skip it.
if (!rankPermissions.isEmpty()) {
@ -47,24 +49,36 @@ public class PermissionUtils {
return ImmutableMap.of();
}
private static Map<String, Boolean> convertToMap(List<String> unconvered) {
if (unconvered == null) {
private static Map<String, Boolean> convertFromList(List<String> permissionsList) {
if (permissionsList == null) {
return ImmutableMap.of();
}
Map<String, Boolean> result = new HashMap<>();
Map<String, Boolean> permissionsMap = new HashMap<>();
for (String permission : unconvered) {
boolean negate = permission.startsWith("-");
if (negate) {
result.put(permission.substring(1), false);
permissionsList.forEach((permission) -> {
if (permission.startsWith("-")) {
permissionsMap.put(permission.substring(1), false);
} else {
result.put(permission, true);
permissionsMap.put(permission, true);
}
});
return permissionsMap;
}
return result;
public static List<String> convertToList(Map<String, Boolean> permissionsMap) {
if (permissionsMap == null) {
return ImmutableList.of();
}
List<String> permissionsList = new LinkedList<>();
permissionsMap.forEach((permission, granted) -> {
permissionsList.add((granted ? "" : "-") + permission);
});
return permissionsList;
}
}