More work

This commit is contained in:
Colin McDonald 2016-05-05 16:00:32 -04:00
parent 03bc01e5ce
commit 04e4db8fcd
26 changed files with 322 additions and 102 deletions

View File

@ -8,6 +8,8 @@ redis.port=6379
http.address= http.address=
http.port=80 http.port=80
http.workerThreads=6 http.workerThreads=6
twillio.accountSID=AC9e2f88c5690134d29a56f698de3cd740
twillio.authToken=982592505a171d3be6b0722f5ecacc0e
mandrill.apiKey=0OYtwymqJP6oqvszeJu0vQ mandrill.apiKey=0OYtwymqJP6oqvszeJu0vQ
auth.permittedUserRanks=developer,owner auth.permittedUserRanks=developer,owner
auth.websiteApiKey=RVbp4hY6sCFVaf auth.websiteApiKey=RVbp4hY6sCFVaf

10
pom.xml
View File

@ -112,6 +112,16 @@
<artifactId>morphia-logging-slf4j</artifactId> <artifactId>morphia-logging-slf4j</artifactId>
<version>1.1.0</version> <version>1.1.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4</version>
</dependency>
<dependency> <dependency>
<groupId>com.warrenstrange</groupId> <groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId> <artifactId>googleauth</artifactId>

View File

@ -1,6 +1,7 @@
package net.frozenorb.apiv3; package net.frozenorb.apiv3;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.mongodb.MongoClient; import com.mongodb.MongoClient;
@ -10,6 +11,7 @@ import lombok.Getter;
import net.frozenorb.apiv3.filters.ActorAttributeFilter; import net.frozenorb.apiv3.filters.ActorAttributeFilter;
import net.frozenorb.apiv3.filters.AuthorizationFilter; import net.frozenorb.apiv3.filters.AuthorizationFilter;
import net.frozenorb.apiv3.filters.ContentTypeFilter; import net.frozenorb.apiv3.filters.ContentTypeFilter;
import net.frozenorb.apiv3.models.NotificationTemplate;
import net.frozenorb.apiv3.routes.GETDump; import net.frozenorb.apiv3.routes.GETDump;
import net.frozenorb.apiv3.routes.GETWhoAmI; import net.frozenorb.apiv3.routes.GETWhoAmI;
import net.frozenorb.apiv3.routes.NotFound; import net.frozenorb.apiv3.routes.NotFound;
@ -39,6 +41,7 @@ import net.frozenorb.apiv3.routes.users.*;
import net.frozenorb.apiv3.serialization.FollowAnnotationExclusionStrategy; import net.frozenorb.apiv3.serialization.FollowAnnotationExclusionStrategy;
import net.frozenorb.apiv3.serialization.ObjectIdTypeAdapter; import net.frozenorb.apiv3.serialization.ObjectIdTypeAdapter;
import net.frozenorb.apiv3.unsorted.LoggingExceptionHandler; import net.frozenorb.apiv3.unsorted.LoggingExceptionHandler;
import net.frozenorb.apiv3.unsorted.Notification;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.mongodb.morphia.Datastore; import org.mongodb.morphia.Datastore;
import org.mongodb.morphia.Morphia; import org.mongodb.morphia.Morphia;
@ -164,6 +167,7 @@ public final class APIv3 {
get("/user/:id/grants", new GETUserGrants(), gson::toJson); get("/user/:id/grants", new GETUserGrants(), gson::toJson);
get("/user/:id/ipLog", new GETUserIPLog(), gson::toJson); get("/user/:id/ipLog", new GETUserIPLog(), gson::toJson);
get("/user/:id/requiresTOTP", new GETUserRequiresTOTP(), gson::toJson); get("/user/:id/requiresTOTP", new GETUserRequiresTOTP(), gson::toJson);
get("/user/:id/verifyPassword", new GETUserVerifyPassword(), gson::toJson);
get("/user/:id", new GETUser(), gson::toJson); get("/user/:id", new GETUser(), gson::toJson);
post("/user/:id/verifyTOTP", new POSTUserVerifyTOTP(), gson::toJson); post("/user/:id/verifyTOTP", new POSTUserVerifyTOTP(), gson::toJson);
post("/user/:id:/grant", new POSTUserGrant(), gson::toJson); post("/user/:id:/grant", new POSTUserGrant(), gson::toJson);

View File

@ -1,5 +1,6 @@
package net.frozenorb.apiv3.auditLog; package net.frozenorb.apiv3.auditLog;
import com.google.common.collect.ImmutableMap;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.actors.Actor; import net.frozenorb.apiv3.actors.Actor;
@ -13,7 +14,7 @@ import java.util.Map;
public class AuditLog { public class AuditLog {
public static void log(User performedBy, String performedByIp, Actor actor, AuditLogActionType actionType) { public static void log(User performedBy, String performedByIp, Actor actor, AuditLogActionType actionType) {
log(performedBy, performedByIp, actor, actionType, new Document()); log(performedBy, performedByIp, actor, actionType, ImmutableMap.of());
} }
public static void log(User performedBy, String performedByIp, Actor actor, AuditLogActionType actionType, Map<String, Object> actionData) { public static void log(User performedBy, String performedByIp, Actor actor, AuditLogActionType actionType, Map<String, Object> actionData) {

View File

@ -3,6 +3,8 @@ package net.frozenorb.apiv3.models;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
import lombok.Getter; import lombok.Getter;
import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.actors.Actor;
import net.frozenorb.apiv3.actors.ActorType;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Entity;
import org.mongodb.morphia.annotations.Id; import org.mongodb.morphia.annotations.Id;

View File

@ -2,6 +2,7 @@ package net.frozenorb.apiv3.models;
import lombok.Getter; import lombok.Getter;
import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.serialization.ExcludeFromReplies;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Entity;
import org.mongodb.morphia.annotations.Id; import org.mongodb.morphia.annotations.Id;
@ -14,7 +15,7 @@ import java.util.UUID;
public final class IPLogEntry { public final class IPLogEntry {
@Getter @Id private String id; @Getter @Id private String id;
@Getter @Indexed private UUID user; @Getter @ExcludeFromReplies @Indexed private UUID user;
@Getter @Indexed private String ip; @Getter @Indexed private String ip;
@Getter private Date firstSeen; @Getter private Date firstSeen;
@Getter private Date lastSeen; @Getter private Date lastSeen;

View File

@ -2,6 +2,8 @@ package net.frozenorb.apiv3.models;
import lombok.Getter; import lombok.Getter;
import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.actors.Actor;
import net.frozenorb.apiv3.actors.ActorType;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Entity;
import org.mongodb.morphia.annotations.Id; import org.mongodb.morphia.annotations.Id;
@ -21,7 +23,8 @@ public final class Punishment {
@Getter private UUID addedBy; @Getter private UUID addedBy;
@Getter @Indexed private Date addedAt; @Getter @Indexed private Date addedAt;
@Getter private String addedOn; // TODO: Make this store actor like the audit log? @Getter private String actorName;
@Getter private ActorType actorType;
@Getter private UUID removedBy; @Getter private UUID removedBy;
@Getter private Date removedAt; @Getter private Date removedAt;
@ -33,7 +36,7 @@ public final class Punishment {
public Punishment() {} // For Morphia public Punishment() {} // For Morphia
public Punishment(User target, String reason, PunishmentType type, Date expiresAt, User addedBy, Server addedOn) { public Punishment(User target, String reason, PunishmentType type, Date expiresAt, User addedBy, Actor actor) {
this.id = new ObjectId().toString(); this.id = new ObjectId().toString();
this.target = target.getId(); this.target = target.getId();
this.reason = reason; this.reason = reason;
@ -41,7 +44,8 @@ public final class Punishment {
this.expiresAt = (Date) expiresAt.clone(); this.expiresAt = (Date) expiresAt.clone();
this.addedBy = addedBy.getId(); this.addedBy = addedBy.getId();
this.addedAt = new Date(); this.addedAt = new Date();
this.addedOn = addedOn.getId(); this.actorName = actor.getName();
this.actorType = actor.getType();
} }
public void delete(User removedBy, String reason) { public void delete(User removedBy, String reason) {

View File

@ -2,7 +2,9 @@ package net.frozenorb.apiv3.models;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.serialization.ExcludeFromReplies;
import net.frozenorb.apiv3.utils.PermissionUtils; import net.frozenorb.apiv3.utils.PermissionUtils;
import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Entity;
import org.mongodb.morphia.annotations.Id; import org.mongodb.morphia.annotations.Id;
@ -13,11 +15,10 @@ import java.util.*;
public final class ServerGroup { public final class ServerGroup {
@Getter @Id private String id; @Getter @Id private String id;
@Getter private String displayName; @Getter private boolean isPublic;
// We define these HashSets up here because, in the event they're // We define these HashSets up here because, in the event they're
// empty, Morphia will load them as null, not empty sets. // empty, Morphia will load them as null, not empty sets.
@Getter private Set<String> announcements = new HashSet<>(); @Getter @Setter @ExcludeFromReplies private Set<String> announcements = new HashSet<>();
@Getter private Set<String> chatFilterList = new HashSet<>();
@Getter private Map<String, List<String>> permissions = new HashMap<>(); @Getter private Map<String, List<String>> permissions = new HashMap<>();
public static ServerGroup byId(String id) { public static ServerGroup byId(String id) {
@ -30,26 +31,13 @@ public final class ServerGroup {
public ServerGroup() {} // For Morphia public ServerGroup() {} // For Morphia
public ServerGroup(String id, String displayName) { public ServerGroup(String id, boolean isPublic) {
this.id = id; this.id = id;
this.displayName = displayName; this.isPublic = isPublic;
}
public void setAnnouncements(Set<String> announcements) {
this.announcements = ImmutableSet.copyOf(announcements);
APIv3.getDatastore().save(this);
}
public void setChatFilterList(Set<String> chatFilterList) {
this.chatFilterList = ImmutableSet.copyOf(chatFilterList);
APIv3.getDatastore().save(this);
} }
public Map<String, Boolean> calculatePermissions(Rank userRank) { public Map<String, Boolean> calculatePermissions(Rank userRank) {
return PermissionUtils.mergePermissions( return PermissionUtils.mergeUpTo(permissions, userRank);
PermissionUtils.getDefaultPermissions(userRank),
PermissionUtils.mergeUpTo(permissions, userRank)
);
} }
public void delete() { public void delete() {

View File

@ -6,6 +6,7 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.serialization.ExcludeFromReplies; import net.frozenorb.apiv3.serialization.ExcludeFromReplies;
import net.frozenorb.apiv3.utils.PermissionUtils;
import net.frozenorb.apiv3.utils.TimeUtils; import net.frozenorb.apiv3.utils.TimeUtils;
import org.bson.Document; import org.bson.Document;
import org.mindrot.jbcrypt.BCrypt; import org.mindrot.jbcrypt.BCrypt;
@ -16,7 +17,7 @@ import org.mongodb.morphia.annotations.Indexed;
import java.util.*; import java.util.*;
@Entity(value = "users", noClassnameStored = true) @Entity(value = "users", noClassnameStored = true)
public final class User { public final class User {
@Getter @Id private UUID id; @Getter @Id private UUID id;
@Getter @Indexed private String lastUsername; @Getter @Indexed private String lastUsername;
@ -26,7 +27,7 @@ public final class User {
@Getter @ExcludeFromReplies @Setter private Date emailTokenSet; @Getter @ExcludeFromReplies @Setter private Date emailTokenSet;
@Getter @ExcludeFromReplies private String password; @Getter @ExcludeFromReplies private String password;
@Getter @Setter private String email; @Getter @Setter private String email;
@Getter private int phoneNumber; @Getter private String phoneNumber;
@Getter private String lastSeenOn; @Getter private String lastSeenOn;
@Getter private Date lastSeenAt; @Getter private Date lastSeenAt;
@Getter private Date firstSeen; @Getter private Date firstSeen;
@ -61,7 +62,7 @@ public final class User {
this.totpSecret = null; this.totpSecret = null;
this.password = null; this.password = null;
this.email = null; this.email = null;
this.phoneNumber = -1; this.phoneNumber = null;
this.lastSeenOn = null; this.lastSeenOn = null;
this.lastSeenAt = new Date(); this.lastSeenAt = new Date();
this.firstSeen = new Date(); this.firstSeen = new Date();
@ -70,14 +71,29 @@ public final class User {
} }
public boolean hasPermissionScoped(String permission, ServerGroup scope) { public boolean hasPermissionScoped(String permission, ServerGroup scope) {
Map<String, Boolean> permissions = scope.calculatePermissions(getHighestRank(scope)); Rank highestRank = getHighestRank(scope);
return permissions.containsKey(permission) && permissions.get(permission); Map<String, Boolean> scopedPermissions = PermissionUtils.mergePermissions(
PermissionUtils.getDefaultPermissions(highestRank),
scope.calculatePermissions(highestRank)
);
return scopedPermissions.containsKey(permission) && scopedPermissions.get(permission);
} }
// TODO
public boolean hasPermissionAnywhere(String permission) { public boolean hasPermissionAnywhere(String permission) {
Map<String, Boolean> permissions = /*scope.calculatePermissions(getHighestRank(scope));*/ ImmutableMap.of(); Map<String, Boolean> globalPermissions = PermissionUtils.getDefaultPermissions(getHighestRank());
return permissions.containsKey(permission) && permissions.get(permission);
for (Map.Entry<ServerGroup, Rank> serverGroupEntry : getHighestRanks().entrySet()) {
ServerGroup serverGroup = serverGroupEntry.getKey();
Rank rank = serverGroupEntry.getValue();
globalPermissions = PermissionUtils.mergePermissions(
globalPermissions,
serverGroup.calculatePermissions(rank)
);
}
return globalPermissions.containsKey(permission) && globalPermissions.get(permission);
} }
public List<Grant> getGrants() { public List<Grant> getGrants() {
@ -122,9 +138,10 @@ public final class User {
} }
} }
public void seenOnServer(Server server) { public void seenOnServer(String username, Server server) {
this.lastSeenOn = server.getId(); this.lastSeenOn = server.getId();
this.lastSeenAt = new Date(); this.lastSeenAt = new Date();
this.aliases.put(username, new Date());
} }
public void setPassword(char[] unencrypted) { public void setPassword(char[] unencrypted) {
@ -135,6 +152,10 @@ public final class User {
return BCrypt.checkpw(new String(unencrypted), password); return BCrypt.checkpw(new String(unencrypted), password);
} }
public Rank getHighestRank() {
return getHighestRank(null);
}
public Rank getHighestRank(ServerGroup serverGroup) { public Rank getHighestRank(ServerGroup serverGroup) {
Rank highest = null; Rank highest = null;
@ -157,8 +178,30 @@ public final class User {
} }
} }
public Rank getHighestRank() { public Map<ServerGroup, Rank> getHighestRanks() {
return getHighestRank(null); Map<ServerGroup, Rank> highestRanks = new HashMap<>();
Rank defaultRank = Rank.byId("default");
List<Grant> userGrants = getGrants();
for (ServerGroup serverGroup : ServerGroup.values()) {
Rank highest = defaultRank;
for (Grant grant : userGrants) {
if (!grant.isActive() || !grant.appliesOn(serverGroup)) {
continue;
}
Rank rank = Rank.byId(grant.getRank());
if (highest == null || rank.getWeight() > highest.getWeight()) {
highest = rank;
}
}
highestRanks.put(serverGroup, highest);
}
return highestRanks;
} }
public Map<String, Object> getLoginInfo(Server server) { public Map<String, Object> getLoginInfo(Server server) {
@ -187,18 +230,21 @@ public final class User {
ServerGroup actorGroup = ServerGroup.byId(server.getGroup()); ServerGroup actorGroup = ServerGroup.byId(server.getGroup());
Rank rank = getHighestRank(actorGroup); Rank highestRank = getHighestRank(actorGroup);
Map<String, Boolean> rankPermissions = actorGroup.calculatePermissions(rank); Map<String, Boolean> scopedPermissions = PermissionUtils.mergePermissions(
PermissionUtils.getDefaultPermissions(highestRank),
actorGroup.calculatePermissions(highestRank)
);
return ImmutableMap.of( return ImmutableMap.of(
"user", this, "user", this,
"access", ImmutableMap.of( "access", ImmutableMap.of(
"allowed", accessDenialReason == null, "allowed", accessDenialReason == null,
"reason", accessDenialReason == null ? "Public server" : accessDenialReason "message", accessDenialReason == null ? "Public server" : accessDenialReason
), ),
"rank", rank, "rank", highestRank.getId(),
"permissions", rankPermissions, "permissions", scopedPermissions,
"totpRequired", getTotpSecret() != null "totpSetup", getTotpSecret() != null
); );
} }

View File

@ -1,5 +1,6 @@
package net.frozenorb.apiv3.models; package net.frozenorb.apiv3.models;
import com.google.common.collect.ImmutableMap;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.APIv3;
@ -9,6 +10,7 @@ import org.mongodb.morphia.annotations.Entity;
import org.mongodb.morphia.annotations.Id; import org.mongodb.morphia.annotations.Id;
import org.mongodb.morphia.annotations.Indexed; import org.mongodb.morphia.annotations.Indexed;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
@Entity(value = "userMeta", noClassnameStored = true) @Entity(value = "userMeta", noClassnameStored = true)
@ -17,15 +19,15 @@ public final class UserMetaEntry {
@Getter @Id private String id; @Getter @Id private String id;
@Getter @Indexed private UUID user; @Getter @Indexed private UUID user;
@Getter @Indexed private String serverGroup; @Getter @Indexed private String serverGroup;
@Getter @Setter private Document data; @Getter @Setter private Map<String, Object> data;
public UserMetaEntry() {} // For Morphia public UserMetaEntry() {} // For Morphia
public UserMetaEntry(User user, ServerGroup serverGroup, Document data) { public UserMetaEntry(User user, ServerGroup serverGroup, Map<String, Object> data) {
this.id = new ObjectId().toString(); this.id = new ObjectId().toString();
this.user = user.getId(); this.user = user.getId();
this.serverGroup = serverGroup.getId(); this.serverGroup = serverGroup.getId();
this.data = new Document(data); this.data = ImmutableMap.copyOf(data);
} }
public void delete() { public void delete() {

View File

@ -1,5 +1,6 @@
package net.frozenorb.apiv3.routes; package net.frozenorb.apiv3.routes;
import com.google.common.collect.ImmutableSet;
import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.models.Grant; import net.frozenorb.apiv3.models.Grant;
import net.frozenorb.apiv3.models.Punishment; import net.frozenorb.apiv3.models.Punishment;
@ -8,8 +9,7 @@ import spark.Request;
import spark.Response; import spark.Response;
import spark.Route; import spark.Route;
import java.util.ArrayList; import java.util.*;
import java.util.List;
public final class GETDump implements Route { public final class GETDump implements Route {
@ -21,27 +21,49 @@ public final class GETDump implements Route {
case "ban": case "ban":
case "mute": case "mute":
case "warn": case "warn":
List<Punishment> activePunishments = new ArrayList<>(); List<UUID> effectedUsers = new ArrayList<>();
APIv3.getDatastore().createQuery(Punishment.class).field("type").equal(type.toUpperCase()).forEach((punishment) -> { APIv3.getDatastore().createQuery(Punishment.class).field("type").equal(type.toUpperCase()).forEach((punishment) -> {
if (punishment.isActive()) { if (punishment.isActive()) {
activePunishments.add(punishment); effectedUsers.add(punishment.getTarget());
} }
}); });
return activePunishments; return effectedUsers;
case "accessDeniable":
// We have to name it effectedUsers2 because Java's
// scoping in switch statements is really dumb.
List<UUID> effectedUsers2 = new ArrayList<>();
APIv3.getDatastore().createQuery(Punishment.class).field("type").in(ImmutableSet.of(
Punishment.PunishmentType.BAN,
Punishment.PunishmentType.BLACKLIST
)).forEach((punishment) -> {
if (punishment.isActive()) {
effectedUsers2.add(punishment.getTarget());
}
});
return effectedUsers2;
case "grant": case "grant":
List<Grant> activeGrants = new ArrayList<>(); Map<String, List<UUID>> grantDump = new HashMap<>();
APIv3.getDatastore().createQuery(Grant.class).forEach((grant) -> { APIv3.getDatastore().createQuery(Grant.class).forEach((grant) -> {
if (grant.isActive()) { if (grant.isActive()) {
activeGrants.add(grant); List<UUID> users = grantDump.get(grant.getRank());
if (users == null) {
users = new ArrayList<>();
grantDump.put(grant.getRank(), users);
}
users.add(grant.getTarget());
} }
}); });
return activeGrants; return grantDump;
default: default:
return ErrorUtils.invalidInput(type + " is not a valid type. Not in [blacklist, ban, mute, warn, grant]"); return ErrorUtils.invalidInput(type + " is not a valid type. Not in [blacklist, ban, mute, warn, accessDeniable, grant]");
} }
} }

View File

@ -1,5 +1,6 @@
package net.frozenorb.apiv3.routes.chatFilterList; package net.frozenorb.apiv3.routes.chatFilterList;
import com.google.common.collect.ImmutableSet;
import net.frozenorb.apiv3.actors.Actor; import net.frozenorb.apiv3.actors.Actor;
import net.frozenorb.apiv3.actors.ActorType; import net.frozenorb.apiv3.actors.ActorType;
import net.frozenorb.apiv3.models.Server; import net.frozenorb.apiv3.models.Server;
@ -12,16 +13,7 @@ import spark.Route;
public final class GETChatFilterList implements Route { public final class GETChatFilterList implements Route {
public Object handle(Request req, Response res) { public Object handle(Request req, Response res) {
Actor actor = req.attribute("actor"); return ImmutableSet.of();
if (actor.getType() != ActorType.SERVER) {
return ErrorUtils.serverOnly();
}
Server sender = Server.byId(actor.getName());
ServerGroup senderGroup = ServerGroup.byId(sender.getGroup());
return senderGroup.getChatFilterList();
} }
} }

View File

@ -1,5 +1,6 @@
package net.frozenorb.apiv3.routes.grants; package net.frozenorb.apiv3.routes.grants;
import com.google.common.collect.ImmutableMap;
import net.frozenorb.apiv3.auditLog.AuditLog; import net.frozenorb.apiv3.auditLog.AuditLog;
import net.frozenorb.apiv3.auditLog.AuditLogActionType; import net.frozenorb.apiv3.auditLog.AuditLogActionType;
import net.frozenorb.apiv3.models.Grant; import net.frozenorb.apiv3.models.Grant;
@ -39,7 +40,7 @@ public final class DELETEGrant implements Route {
grant.delete(removedBy, reason); grant.delete(removedBy, reason);
// TODO: Fix IP // TODO: Fix IP
AuditLog.log(removedBy, "", req.attribute("actor"), AuditLogActionType.DELETE_GRANT, new Document("grantId", grant.getId())); AuditLog.log(removedBy, "", req.attribute("actor"), AuditLogActionType.DELETE_GRANT, ImmutableMap.of());
return grant; return grant;
} }

View File

@ -1,5 +1,6 @@
package net.frozenorb.apiv3.routes.punishments; package net.frozenorb.apiv3.routes.punishments;
import com.google.common.collect.ImmutableMap;
import net.frozenorb.apiv3.auditLog.AuditLog; import net.frozenorb.apiv3.auditLog.AuditLog;
import net.frozenorb.apiv3.auditLog.AuditLogActionType; import net.frozenorb.apiv3.auditLog.AuditLogActionType;
import net.frozenorb.apiv3.models.Punishment; import net.frozenorb.apiv3.models.Punishment;
@ -31,7 +32,7 @@ public final class DELETEPunishment implements Route {
return ErrorUtils.unauthorized(requiredPermission); return ErrorUtils.unauthorized(requiredPermission);
} }
String reason = req.queryParams("removalReason"); String reason = req.queryParams("reason");
if (reason == null || reason.trim().isEmpty()) { if (reason == null || reason.trim().isEmpty()) {
return ErrorUtils.requiredInput("reason"); return ErrorUtils.requiredInput("reason");
@ -39,7 +40,7 @@ public final class DELETEPunishment implements Route {
punishment.delete(removedBy, reason); punishment.delete(removedBy, reason);
// TODO: Fix IP // TODO: Fix IP
AuditLog.log(removedBy, "", req.attribute("actor"), AuditLogActionType.DELETE_PUNISHMENT, new Document("punishmentId", punishment.getId())); AuditLog.log(removedBy, "", req.attribute("actor"), AuditLogActionType.DELETE_PUNISHMENT, ImmutableMap.of());
return punishment; return punishment;
} }

View File

@ -17,8 +17,6 @@ public final class POSTUserPunish implements Route {
public Object handle(Request req, Response res) { public Object handle(Request req, Response res) {
User target = User.byId(req.params("id")); User target = User.byId(req.params("id"));
// TODO: PROTECTED USERS
if (target == null) { if (target == null) {
return ErrorUtils.notFound("User", req.params("id")); return ErrorUtils.notFound("User", req.params("id"));
} }
@ -45,13 +43,11 @@ public final class POSTUserPunish implements Route {
return ErrorUtils.unauthorized(requiredPermission); return ErrorUtils.unauthorized(requiredPermission);
} }
Server addedOn = Server.byId(req.queryParams("addedOn")); if (target.hasPermissionAnywhere(Permissions.PROTECTED_PUNISHMENT)) {
return ErrorUtils.error(target.getLastSeenOn() + " is protected from punishments.");
if (addedOn == null) {
return ErrorUtils.notFound("Server", req.queryParams("addedOn"));
} }
Punishment punishment = new Punishment(target, reason, type, expiresAt, addedBy, addedOn); Punishment punishment = new Punishment(target, reason, type, expiresAt, addedBy, req.attribute("actor"));
APIv3.getDatastore().save(punishment); APIv3.getDatastore().save(punishment);
return punishment; return punishment;
} }

View File

@ -10,9 +10,9 @@ public final class POSTServerGroup implements Route {
public Object handle(Request req, Response res) { public Object handle(Request req, Response res) {
String id = req.queryParams("id"); String id = req.queryParams("id");
String displayName = req.queryParams("displayName"); boolean isPublic = Boolean.valueOf(req.queryParams("public"));
ServerGroup serverGroup = new ServerGroup(id, displayName); ServerGroup serverGroup = new ServerGroup(id, isPublic);
APIv3.getDatastore().save(serverGroup); APIv3.getDatastore().save(serverGroup);
return serverGroup; return serverGroup;
} }

View File

@ -39,7 +39,7 @@ public final class POSTServerHeartbeat implements Route {
user = new User(UUID.fromString(playerJson.getString("uuid")), username); user = new User(UUID.fromString(playerJson.getString("uuid")), username);
} }
user.seenOnServer(actorServer); user.seenOnServer(username, actorServer);
APIv3.getDatastore().save(user); APIv3.getDatastore().save(user);
onlinePlayers.add(user.getId()); onlinePlayers.add(user.getId());

View File

@ -16,13 +16,14 @@ public final class GETUserDetails implements Route {
return ErrorUtils.notFound("User", req.params("id")); return ErrorUtils.notFound("User", req.params("id"));
} }
return ImmutableMap.of( // Too many fields to use .of()
"user", user, return ImmutableMap.builder()
"grants", user.getGrants(), .put("user", user)
"ipLog", user.getIPLog(), .put("grants", user.getGrants())
"punishments", user.getPunishments(), .put("ipLog", user.getIPLog())
"totpRequired", user.getTotpSecret() != null .put("punishments", user.getPunishments())
); .put("aliases", user.getAliases())
.put("totpSetup", user.getTotpSecret() != null);
} }
} }

View File

@ -1,4 +1,47 @@
package net.frozenorb.apiv3.routes.users; package net.frozenorb.apiv3.routes.users;
public class GETUserRequiresTOTP { import com.google.common.collect.ImmutableMap;
} import net.frozenorb.apiv3.models.User;
import net.frozenorb.apiv3.utils.ErrorUtils;
import net.frozenorb.apiv3.utils.IPUtils;
import net.frozenorb.apiv3.utils.TOTPUtils;
import spark.Request;
import spark.Response;
import spark.Route;
public final class GETUserRequiresTOTP implements Route {
public Object handle(Request req, Response res) {
User user = User.byId(req.params("id"));
if (user == null) {
return ErrorUtils.notFound("User", req.params("id"));
}
if (user.getTotpSecret() == null) {
return ImmutableMap.of(
"required", false,
"message", "User does not have TOTP setup."
);
}
String userIp = req.queryParams("userIp");
if (!IPUtils.isValidIP(userIp)) {
return ErrorUtils.invalidInput("IP address \"" + userIp + "\" is not valid.");
}
if (TOTPUtils.isPreAuthorized(user, userIp)) {
return ImmutableMap.of(
"required", false,
"message", "User's IP has already been validated"
);
}
return ImmutableMap.of(
"required", true,
"message", "User has no TOTP exemptions."
);
}
}

View File

@ -0,0 +1,35 @@
package net.frozenorb.apiv3.routes.users;
import com.google.common.collect.ImmutableMap;
import net.frozenorb.apiv3.models.User;
import net.frozenorb.apiv3.utils.ErrorUtils;
import net.frozenorb.apiv3.utils.IPUtils;
import net.frozenorb.apiv3.utils.TOTPUtils;
import spark.Request;
import spark.Response;
import spark.Route;
import java.util.concurrent.TimeUnit;
public final class GETUserVerifyPassword implements Route {
public Object handle(Request req, Response res) {
User user = User.byId(req.params("id"));
if (user == null) {
return ErrorUtils.notFound("User", req.params("id"));
}
if (user.getPassword() == null) {
return ErrorUtils.invalidInput("User provided does not have password set.");
}
char[] password = req.queryParams("password").toCharArray();
boolean authorized = user.checkPassword(password);
return ImmutableMap.of(
"authorized", authorized
);
}
}

View File

@ -39,16 +39,16 @@ public final class POSTUserConfirmRegister implements Route {
return ErrorUtils.error("Email token is expired"); return ErrorUtils.error("Email token is expired");
} }
String password = req.queryParams("password"); char[] password = req.queryParams("password").toCharArray();
if (password.length() < 8) { if (password.length < 8) {
return ErrorUtils.error("Your password is too short."); return ErrorUtils.error("Your password is too short.");
} else if (commonPasswords.contains(password)) { } else if (commonPasswords.contains(new String(password))) {
return ErrorUtils.error("Your password is too common. Please use a more secure password."); return ErrorUtils.error("Your password is too common. Please use a more secure password.");
} }
user.setEmailToken(null); user.setEmailToken(null);
user.setPassword(password.toCharArray()); user.setPassword(password);
APIv3.getDatastore().save(user); APIv3.getDatastore().save(user);
return ImmutableMap.of( return ImmutableMap.of(

View File

@ -19,11 +19,10 @@ import java.util.regex.Pattern;
public final class POSTUserRegister implements Route { public final class POSTUserRegister implements Route {
private static final Pattern VALID_EMAIL_PATTERN = private static final Pattern VALID_EMAIL_PATTERN = Pattern.compile(
Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE); "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$",
Pattern.CASE_INSENSITIVE
// TODO: POSSIBLE? perms check );
// TODO: make messages better
public Object handle(Request req, Response res) { public Object handle(Request req, Response res) {
User user = User.byId(req.params("id")); User user = User.byId(req.params("id"));

View File

@ -3,11 +3,14 @@ package net.frozenorb.apiv3.routes.users;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import net.frozenorb.apiv3.models.User; import net.frozenorb.apiv3.models.User;
import net.frozenorb.apiv3.utils.ErrorUtils; import net.frozenorb.apiv3.utils.ErrorUtils;
import net.frozenorb.apiv3.utils.IPUtils;
import net.frozenorb.apiv3.utils.TOTPUtils; import net.frozenorb.apiv3.utils.TOTPUtils;
import spark.Request; import spark.Request;
import spark.Response; import spark.Response;
import spark.Route; import spark.Route;
import java.util.concurrent.TimeUnit;
public final class POSTUserVerifyTOTP implements Route { public final class POSTUserVerifyTOTP implements Route {
public Object handle(Request req, Response res) { public Object handle(Request req, Response res) {
@ -21,11 +24,37 @@ public final class POSTUserVerifyTOTP implements Route {
return ErrorUtils.invalidInput("User provided does not have TOTP code set."); return ErrorUtils.invalidInput("User provided does not have TOTP code set.");
} }
String userIp = req.queryParams("userIp");
if (!IPUtils.isValidIP(userIp)) {
return ErrorUtils.invalidInput("IP address \"" + userIp + "\" is not valid.");
}
int providedCode = Integer.parseInt(req.queryParams("code")); int providedCode = Integer.parseInt(req.queryParams("code"));
return ImmutableMap.of( if (TOTPUtils.wasRecentlyUsed(user, providedCode)) {
"verified", TOTPUtils.authorizeUser(user, providedCode) return ImmutableMap.of(
); "authorized", false,
"message", "TOTP code was recently used."
);
}
boolean authorized = TOTPUtils.authorizeUser(user, providedCode);
if (authorized) {
TOTPUtils.markPreAuthorized(user, userIp, 3, TimeUnit.DAYS);
TOTPUtils.markRecentlyUsed(user, providedCode);
return ImmutableMap.of(
"authorized", true,
"message", "Valid TOTP code provided."
);
} else {
return ImmutableMap.of(
"authorized", false,
"message", "TOTP code was not valid."
);
}
} }
} }

View File

@ -5,16 +5,25 @@ import com.cribbstechnologies.clients.mandrill.model.MandrillHtmlMessage;
import com.cribbstechnologies.clients.mandrill.model.MandrillMessageRequest; import com.cribbstechnologies.clients.mandrill.model.MandrillMessageRequest;
import com.cribbstechnologies.clients.mandrill.model.MandrillRecipient; import com.cribbstechnologies.clients.mandrill.model.MandrillRecipient;
import com.cribbstechnologies.clients.mandrill.request.MandrillMessagesRequest; import com.cribbstechnologies.clients.mandrill.request.MandrillMessagesRequest;
import com.twilio.sdk.TwilioRestException;
import com.twilio.sdk.resource.factory.MessageFactory;
import com.twilio.sdk.resource.instance.Message;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.models.NotificationTemplate; import net.frozenorb.apiv3.models.NotificationTemplate;
import net.frozenorb.apiv3.utils.MandrillUtils; import net.frozenorb.apiv3.utils.MandrillUtils;
import sun.reflect.generics.reflectiveObjects.NotImplementedException; import net.frozenorb.apiv3.utils.TwillioUtils;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map; import java.util.Map;
public final class Notification { public final class Notification {
private static final MandrillMessagesRequest messagesRequest = MandrillUtils.createMessagesRequest(); private static final MandrillMessagesRequest mandrillMessagesRequest = MandrillUtils.createMessagesRequest();
private static final MessageFactory twillioMessageFactory = TwillioUtils.createMessageFactory();
private final String subject; private final String subject;
private final String body; private final String body;
@ -38,15 +47,24 @@ public final class Notification {
try { try {
MandrillMessageRequest request = new MandrillMessageRequest(); MandrillMessageRequest request = new MandrillMessageRequest();
request.setMessage(message); request.setMessage(message);
messagesRequest.sendMessage(request); mandrillMessagesRequest.sendMessage(request);
} catch (RequestFailedException ex) { } catch (RequestFailedException ex) {
throw new IOException("Failed to send notification to user", ex); throw new IOException("Failed to send notification to user", ex);
} }
} }
public void sendAsText(String phoneNumber) throws IOException { public void sendAsText(String phoneNumber) throws IOException {
// TODO List<NameValuePair> params = new ArrayList<>();
throw new IOException(new NotImplementedException());
params.add(new BasicNameValuePair("To", phoneNumber));
params.add(new BasicNameValuePair("From", "+13108795180"));
params.add(new BasicNameValuePair("Body", body));
try {
twillioMessageFactory.create(params);
} catch (TwilioRestException ex) {
throw new IOException("Failed to send notification to user", ex);
}
} }
} }

View File

@ -10,5 +10,6 @@ public class Permissions {
public static final String REMOVE_PUNISHMENT = "minehq.punishment.remove"; // minehq.punishment.remove.%TYPE% public static final String REMOVE_PUNISHMENT = "minehq.punishment.remove"; // minehq.punishment.remove.%TYPE%
public static final String CREATE_PUNISHMENT = "minehq.punishment.create"; // minehq.punishment.create.%TYPE% public static final String CREATE_PUNISHMENT = "minehq.punishment.create"; // minehq.punishment.create.%TYPE%
public static final String PROTECTED_PUNISHMENT = "minehq.punishment.protected";
} }

View File

@ -0,0 +1,22 @@
package net.frozenorb.apiv3.utils;
import com.twilio.sdk.TwilioRestClient;
import com.twilio.sdk.resource.factory.MessageFactory;
import com.twilio.sdk.resource.instance.Account;
import lombok.experimental.UtilityClass;
import net.frozenorb.apiv3.APIv3;
@UtilityClass
public class TwillioUtils {
public static MessageFactory createMessageFactory() {
TwilioRestClient twillioClient = new TwilioRestClient(
APIv3.getConfig().getProperty("twillio.accountSID"),
APIv3.getConfig().getProperty("twillio.authToken")
);
Account account = twillioClient.getAccount();
return account.getMessageFactory();
}
}