Whoa stuff

This commit is contained in:
Colin McDonald 2016-05-05 19:35:45 -04:00
parent 1b05b16fed
commit 6009522090
17 changed files with 169 additions and 62 deletions

View File

@ -24,10 +24,7 @@ import net.frozenorb.apiv3.routes.notificationTemplate.DELETENotificationTemplat
import net.frozenorb.apiv3.routes.notificationTemplate.GETNotificationTemplate; import net.frozenorb.apiv3.routes.notificationTemplate.GETNotificationTemplate;
import net.frozenorb.apiv3.routes.notificationTemplate.GETNotificationTemplates; import net.frozenorb.apiv3.routes.notificationTemplate.GETNotificationTemplates;
import net.frozenorb.apiv3.routes.notificationTemplate.POSTNotificationTemplate; import net.frozenorb.apiv3.routes.notificationTemplate.POSTNotificationTemplate;
import net.frozenorb.apiv3.routes.punishments.DELETEPunishment; import net.frozenorb.apiv3.routes.punishments.*;
import net.frozenorb.apiv3.routes.punishments.GETPunishment;
import net.frozenorb.apiv3.routes.punishments.GETPunishments;
import net.frozenorb.apiv3.routes.punishments.POSTUserPunish;
import net.frozenorb.apiv3.routes.ranks.DELETERank; import net.frozenorb.apiv3.routes.ranks.DELETERank;
import net.frozenorb.apiv3.routes.ranks.GETRank; import net.frozenorb.apiv3.routes.ranks.GETRank;
import net.frozenorb.apiv3.routes.ranks.GETRanks; import net.frozenorb.apiv3.routes.ranks.GETRanks;
@ -146,12 +143,14 @@ public final class APIv3 {
private void setupHttp() { private void setupHttp() {
ipAddress(config.getProperty("http.address")); ipAddress(config.getProperty("http.address"));
port(Integer.parseInt(config.getProperty("http.port"))); port(Integer.parseInt(config.getProperty("http.port")));
// TODO: if threadPool == null use default value
threadPool(Integer.parseInt(config.getProperty("http.workerThreads"))); threadPool(Integer.parseInt(config.getProperty("http.workerThreads")));
before(new ContentTypeFilter()); before(new ContentTypeFilter());
before(new ActorAttributeFilter()); before(new ActorAttributeFilter());
before(new AuthorizationFilter()); before(new AuthorizationFilter());
before(new MetricsBeforeFilter()); before(new MetricsBeforeFilter());
after(new MetricsAfterFilter()); after(new MetricsAfterFilter());
after(new LoggingFilter());
exception(Exception.class, new LoggingExceptionHandler()); exception(Exception.class, new LoggingExceptionHandler());
// TODO: The commented out routes // TODO: The commented out routes
@ -200,13 +199,14 @@ public final class APIv3 {
get("/user/:id/details", new GETUserDetails(), gson::toJson); get("/user/:id/details", new GETUserDetails(), gson::toJson);
get("/user/:id/meta/:serverGroup", new GETUserMeta(), gson::toJson); get("/user/:id/meta/:serverGroup", new GETUserMeta(), gson::toJson);
get("/user/:id/grants", new GETUserGrants(), gson::toJson); get("/user/:id/grants", new GETUserGrants(), gson::toJson);
get("/user/:id/punishments", new GETUserPunishments(), 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/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);
post("/user/:id:/punish", new POSTUserPunish(), gson::toJson); post("/user/:id/punish", new POSTUserPunish(), gson::toJson);
post("/user/:id/login", new POSTUserLogin(), gson::toJson); post("/user/:id/login", new POSTUserLogin(), gson::toJson);
post("/user/:id/notify", new POSTUserNotify(), gson::toJson); post("/user/:id/notify", new POSTUserNotify(), gson::toJson);
post("/user/:id/register", new POSTUserRegister(), gson::toJson); post("/user/:id/register", new POSTUserRegister(), gson::toJson);
@ -214,6 +214,7 @@ public final class APIv3 {
post("/user/confirmRegister/:emailToken", new POSTUserConfirmRegister(), gson::toJson); post("/user/confirmRegister/:emailToken", new POSTUserConfirmRegister(), gson::toJson);
put("/user/:id/meta/:serverGroup", new PUTUserMeta(), gson::toJson); put("/user/:id/meta/:serverGroup", new PUTUserMeta(), gson::toJson);
delete("/user/:id/meta/:serverGroup", new DELETEUserMeta(), gson::toJson); delete("/user/:id/meta/:serverGroup", new DELETEUserMeta(), gson::toJson);
delete("/user/:id/punishment", new DELETEUserPunishment(), gson::toJson);
// There's no way to do a JSON 404 page w/o doing this :( // There's no way to do a JSON 404 page w/o doing this :(
get("/*", new NotFound(), gson::toJson); get("/*", new NotFound(), gson::toJson);

View File

@ -0,0 +1,23 @@
package net.frozenorb.apiv3.filters;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import lombok.extern.slf4j.Slf4j;
import net.frozenorb.apiv3.APIv3;
import spark.Filter;
import spark.Request;
import spark.Response;
@Slf4j
public final class LoggingFilter implements Filter {
public void handle(Request req, Response res) {
if (req.url().toLowerCase().contains("password=")) {
return;
}
log.info(req.requestMethod().toUpperCase() + " " + req.url());
}
}

View File

@ -8,15 +8,18 @@ import spark.Filter;
import spark.Request; import spark.Request;
import spark.Response; import spark.Response;
import java.util.concurrent.TimeUnit;
public final class MetricsAfterFilter implements Filter { public final class MetricsAfterFilter implements Filter {
private Histogram responseLengthMetric = APIv3.getMetrics().histogram(MetricRegistry.name("apiv3", "http", "responseLength")); private Histogram responseLengthMetric = APIv3.getMetrics().histogram(MetricRegistry.name("apiv3", "http", "responseLength"));
private Timer responseTimesMetric = APIv3.getMetrics().timer(MetricRegistry.name("apiv3", "http", "responseTimes"));
public void handle(Request req, Response res) { public void handle(Request req, Response res) {
responseLengthMetric.update(req.contentLength()); responseLengthMetric.update(req.contentLength());
Timer.Context timerMetric = req.attribute("timerMetric"); long started = req.attribute("requestStarted");
timerMetric.stop(); responseTimesMetric.update(System.currentTimeMillis() - started, TimeUnit.MILLISECONDS);
} }
} }

View File

@ -9,10 +9,8 @@ import spark.Response;
public final class MetricsBeforeFilter implements Filter { public final class MetricsBeforeFilter implements Filter {
private Timer responseTimesMetric = APIv3.getMetrics().timer(MetricRegistry.name("apiv3", "http", "responseTimes"));
public void handle(Request req, Response res) { public void handle(Request req, Response res) {
req.attribute("timerMetric", responseTimesMetric.time()); req.attribute("requestStarted", System.currentTimeMillis());
} }
} }

View File

@ -19,7 +19,7 @@ public final class Grant {
@Getter @Id private String id; @Getter @Id private String id;
@Getter @Indexed private UUID target; @Getter @Indexed private UUID target;
@Getter private String reason; @Getter private String reason;
@Getter private Set<String> scopes; @Getter private Set<String> scopes = new HashSet<>(); // So on things w/o scopes we still load properly (Morphia drops empty sets)
@Getter @Indexed private String rank; @Getter @Indexed private String rank;
@Getter private Date expiresAt; @Getter private Date expiresAt;
@ -42,8 +42,8 @@ public final class Grant {
this.reason = reason; this.reason = reason;
this.scopes = new HashSet<>(Collections2.transform(scopes, ServerGroup::getId)); this.scopes = new HashSet<>(Collections2.transform(scopes, ServerGroup::getId));
this.rank = rank.getId(); this.rank = rank.getId();
this.expiresAt = (Date) expiresAt.clone(); this.expiresAt = expiresAt;
this.addedBy = addedBy.getId(); this.addedBy = addedBy == null ? null : addedBy.getId();
this.addedAt = new Date(); this.addedAt = new Date();
} }
@ -63,7 +63,7 @@ public final class Grant {
if (expiresAt == null) { if (expiresAt == null) {
return false; // Never expires return false; // Never expires
} else { } else {
return expiresAt.after(new Date()); return expiresAt.before(new Date());
} }
} }

View File

@ -41,8 +41,8 @@ public final class Punishment {
this.target = target.getId(); this.target = target.getId();
this.reason = reason; this.reason = reason;
this.type = type; this.type = type;
this.expiresAt = (Date) expiresAt.clone(); this.expiresAt = expiresAt;
this.addedBy = addedBy.getId(); this.addedBy = addedBy == null ? null : addedBy.getId();
this.addedAt = new Date(); this.addedAt = new Date();
this.actorName = actor.getName(); this.actorName = actor.getName();
this.actorType = actor.getType(); this.actorType = actor.getType();
@ -64,7 +64,7 @@ public final class Punishment {
if (expiresAt == null) { if (expiresAt == null) {
return false; // Never expires return false; // Never expires
} else { } else {
return expiresAt.after(new Date()); return expiresAt.before(new Date());
} }
} }

View File

@ -7,6 +7,7 @@ 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;
import org.mongodb.morphia.annotations.Property;
import java.util.*; import java.util.*;
@ -14,7 +15,8 @@ import java.util.*;
public final class ServerGroup { public final class ServerGroup {
@Getter @Id private String id; @Getter @Id private String id;
@Getter private boolean isPublic; // We rename this to public, we just can't name it that because it's a Java identifier.
@Getter @Property("public") 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 @Setter @ExcludeFromReplies private Set<String> announcements = new HashSet<>(); @Getter @Setter @ExcludeFromReplies private Set<String> announcements = new HashSet<>();

View File

@ -30,7 +30,7 @@ public final class User {
@Getter private String 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 firstSeenAt;
public static User byId(String id) { public static User byId(String id) {
try { try {
@ -65,7 +65,7 @@ public final class User {
this.phoneNumber = null; this.phoneNumber = null;
this.lastSeenOn = null; this.lastSeenOn = null;
this.lastSeenAt = new Date(); this.lastSeenAt = new Date();
this.firstSeen = new Date(); this.firstSeenAt = new Date();
aliases.put(lastUsername, new Date()); aliases.put(lastUsername, new Date());
} }
@ -157,7 +157,7 @@ public final class User {
} }
public Rank getHighestRank(ServerGroup serverGroup) { public Rank getHighestRank(ServerGroup serverGroup) {
Rank highest = null; Rank highest = null;;
for (Grant grant : getGrants()) { for (Grant grant : getGrants()) {
if (!grant.isActive() || (serverGroup != null && !grant.appliesOn(serverGroup))) { if (!grant.isActive() || (serverGroup != null && !grant.appliesOn(serverGroup))) {
@ -231,6 +231,7 @@ public final class User {
ServerGroup actorGroup = ServerGroup.byId(server.getGroup()); ServerGroup actorGroup = ServerGroup.byId(server.getGroup());
Rank highestRank = getHighestRank(actorGroup); Rank highestRank = getHighestRank(actorGroup);
Map<String, Boolean> scopedPermissions = PermissionUtils.mergePermissions( Map<String, Boolean> scopedPermissions = PermissionUtils.mergePermissions(
PermissionUtils.getDefaultPermissions(highestRank), PermissionUtils.getDefaultPermissions(highestRank),
actorGroup.calculatePermissions(highestRank) actorGroup.calculatePermissions(highestRank)

View File

@ -23,12 +23,9 @@ public final class DELETEGrant implements Route {
} }
User removedBy = User.byId(req.queryParams("removedBy")); User removedBy = User.byId(req.queryParams("removedBy"));
String requiredPermission = Permissions.REMOVE_GRANT + "." + grant.getRank();
if (removedBy == null) { if (removedBy == null) {
return ErrorUtils.notFound("User", req.queryParams("removedBy")); return ErrorUtils.notFound("User", req.queryParams("removedBy"));
} else if (!removedBy.hasPermissionAnywhere(requiredPermission)) {
return ErrorUtils.unauthorized(requiredPermission);
} }
String reason = req.queryParams("reason"); String reason = req.queryParams("reason");

View File

@ -31,15 +31,18 @@ public final class POSTUserGrant implements Route {
} }
Set<ServerGroup> scopes = new HashSet<>(); Set<ServerGroup> scopes = new HashSet<>();
String scopesUnparsed = req.queryParams("scopes");
for (String serverGroupId : req.queryParams("scopes").split(",")) { if (!scopesUnparsed.isEmpty()) {
ServerGroup serverGroup = ServerGroup.byId(serverGroupId); for (String serverGroupId : scopesUnparsed.split(",")) {
ServerGroup serverGroup = ServerGroup.byId(serverGroupId);
if (serverGroup == null) { if (serverGroup == null) {
return ErrorUtils.notFound("Server group", serverGroupId); return ErrorUtils.notFound("Server group", serverGroupId);
}
scopes.add(serverGroup);
} }
scopes.add(serverGroup);
} }
Rank rank = Rank.byId(req.queryParams("rank")); Rank rank = Rank.byId(req.queryParams("rank"));
@ -48,20 +51,20 @@ public final class POSTUserGrant implements Route {
return ErrorUtils.notFound("Rank", req.queryParams("rank")); return ErrorUtils.notFound("Rank", req.queryParams("rank"));
} }
Date expiresAt = new Date(Long.parseLong(req.queryParams("expiresAt"))); Date expiresAt;
if (expiresAt.before(new Date())) { try {
expiresAt = new Date(Long.parseLong(req.queryParams("expiresAt")));
} catch (NumberFormatException ex) {
expiresAt = null;
}
if (expiresAt != null && expiresAt.before(new Date())) {
return ErrorUtils.invalidInput("Expiration date cannot be in the past."); return ErrorUtils.invalidInput("Expiration date cannot be in the past.");
} }
// We purposely don't do a null check, grants don't have to have a source.
User addedBy = User.byId(req.queryParams("addedBy")); User addedBy = User.byId(req.queryParams("addedBy"));
String requiredPermission = Permissions.CREATE_GRANT + "." + rank.getId();
if (addedBy == null) {
return ErrorUtils.notFound("User", req.queryParams("addedBy"));
} else if (!addedBy.hasPermissionAnywhere(requiredPermission)) {
return ErrorUtils.unauthorized(requiredPermission);
}
Grant grant = new Grant(target, reason, scopes, rank, expiresAt, addedBy); Grant grant = new Grant(target, reason, scopes, rank, expiresAt, addedBy);
APIv3.getDatastore().save(grant); APIv3.getDatastore().save(grant);

View File

@ -23,12 +23,9 @@ public final class DELETEPunishment implements Route {
} }
User removedBy = User.byId(req.queryParams("removedBy")); User removedBy = User.byId(req.queryParams("removedBy"));
String requiredPermission = Permissions.REMOVE_PUNISHMENT + "." + punishment.getType().name();
if (removedBy == null) { if (removedBy == null) {
return ErrorUtils.notFound("User", req.queryParams("removedBy")); return ErrorUtils.notFound("User", req.queryParams("removedBy"));
} else if (!removedBy.hasPermissionAnywhere(requiredPermission)) {
return ErrorUtils.unauthorized(requiredPermission);
} }
String reason = req.queryParams("reason"); String reason = req.queryParams("reason");

View File

@ -0,0 +1,21 @@
package net.frozenorb.apiv3.routes.punishments;
import net.frozenorb.apiv3.models.User;
import net.frozenorb.apiv3.utils.ErrorUtils;
import spark.Request;
import spark.Response;
import spark.Route;
public final class GETUserPunishments implements Route {
public Object handle(Request req, Response res) {
User target = User.byId(req.params("id"));
if (target == null) {
return ErrorUtils.notFound("User", req.params("id"));
}
return target.getPunishments();
}
}

View File

@ -1,6 +1,10 @@
package net.frozenorb.apiv3.routes.punishments; package net.frozenorb.apiv3.routes.punishments;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.auditLog.AuditLog;
import net.frozenorb.apiv3.auditLog.AuditLogActionType;
import net.frozenorb.apiv3.models.Punishment; import net.frozenorb.apiv3.models.Punishment;
import net.frozenorb.apiv3.models.User; import net.frozenorb.apiv3.models.User;
import net.frozenorb.apiv3.unsorted.Permissions; import net.frozenorb.apiv3.unsorted.Permissions;
@ -27,20 +31,29 @@ public final class POSTUserPunish implements Route {
} }
Punishment.PunishmentType type = Punishment.PunishmentType.valueOf(req.queryParams("type")); Punishment.PunishmentType type = Punishment.PunishmentType.valueOf(req.queryParams("type"));
Date expiresAt = new Date(Long.parseLong(req.queryParams("expiresAt")));
if (expiresAt.before(new Date())) { if (type != Punishment.PunishmentType.WARN) {
for (Punishment punishment : target.getPunishments(ImmutableSet.of(type))) {
if (punishment.isActive()) {
return ErrorUtils.error("A punishment by " + User.byId(punishment.getAddedBy()).getLastUsername() + " already covers this user.");
}
}
}
Date expiresAt;
try {
expiresAt = new Date(Long.parseLong(req.queryParams("expiresAt")));
} catch (NumberFormatException ex) {
expiresAt = null;
}
if (expiresAt != null && expiresAt.before(new Date())) {
return ErrorUtils.invalidInput("Expiration date cannot be in the past."); return ErrorUtils.invalidInput("Expiration date cannot be in the past.");
} }
// We purposely don't do a null check, grants don't have to have a source.
User addedBy = User.byId(req.queryParams("addedBy")); User addedBy = User.byId(req.queryParams("addedBy"));
String requiredPermission = Permissions.CREATE_PUNISHMENT + "." + type.name();
if (addedBy == null) {
return ErrorUtils.notFound("User", req.queryParams("addedBy"));
} else if (!addedBy.hasPermissionAnywhere(requiredPermission)) {
return ErrorUtils.unauthorized(requiredPermission);
}
if (target.hasPermissionAnywhere(Permissions.PROTECTED_PUNISHMENT)) { if (target.hasPermissionAnywhere(Permissions.PROTECTED_PUNISHMENT)) {
return ErrorUtils.error(target.getLastSeenOn() + " is protected from punishments."); return ErrorUtils.error(target.getLastSeenOn() + " is protected from punishments.");

View File

@ -0,0 +1,48 @@
package net.frozenorb.apiv3.routes.users;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import net.frozenorb.apiv3.auditLog.AuditLog;
import net.frozenorb.apiv3.auditLog.AuditLogActionType;
import net.frozenorb.apiv3.models.Punishment;
import net.frozenorb.apiv3.models.User;
import net.frozenorb.apiv3.utils.ErrorUtils;
import spark.Request;
import spark.Response;
import spark.Route;
public final class DELETEUserPunishment implements Route {
public Object handle(Request req, Response res) {
User target = User.byId(req.params("id"));
if (target == null) {
return ErrorUtils.notFound("User", req.params("id"));
}
Punishment.PunishmentType type = Punishment.PunishmentType.valueOf(req.queryParams("type").toUpperCase());
User removedBy = User.byId(req.queryParams("removedBy"));
if (removedBy == null) {
return ErrorUtils.notFound("User", req.queryParams("removedBy"));
}
String reason = req.queryParams("reason");
if (reason == null || reason.trim().isEmpty()) {
return ErrorUtils.requiredInput("reason");
}
for (Punishment punishment : target.getPunishments(ImmutableSet.of(type))) {
if (punishment.isActive()) {
punishment.delete(removedBy, reason);
// TODO: Fix IP
AuditLog.log(removedBy, "", req.attribute("actor"), AuditLogActionType.DELETE_PUNISHMENT, ImmutableMap.of());
return punishment;
}
}
return ErrorUtils.error("User provided has no active punishments");
}
}

View File

@ -24,18 +24,18 @@ public final class GETStaff implements Route {
} }
}); });
Map<Rank, Set<User>> result = new HashMap<>(); Map<String, Set<User>> result = new HashMap<>();
APIv3.getDatastore().createQuery(Grant.class).field("rank").in(staffRanks.keySet()).forEach(grant -> { APIv3.getDatastore().createQuery(Grant.class).field("rank").in(staffRanks.keySet()).forEach(grant -> {
if (grant.isActive()) { if (grant.isActive()) {
User user = User.byId(grant.getTarget()); User user = User.byId(grant.getTarget());
Rank rank = staffRanks.get(grant.getRank()); Rank rank = staffRanks.get(grant.getRank());
if (!result.containsKey(rank)) { if (!result.containsKey(rank.getId())) {
result.put(rank, new HashSet<>()); result.put(rank.getId(), new HashSet<>());
} }
result.get(rank).add(user); result.get(rank.getId()).add(user);
} }
}); });

View File

@ -23,7 +23,8 @@ public final class GETUserDetails implements Route {
.put("ipLog", user.getIPLog()) .put("ipLog", user.getIPLog())
.put("punishments", user.getPunishments()) .put("punishments", user.getPunishments())
.put("aliases", user.getAliases()) .put("aliases", user.getAliases())
.put("totpSetup", user.getTotpSecret() != null); .put("totpSetup", user.getTotpSecret() != null)
.build();
} }
} }

View File

@ -2,6 +2,7 @@ package net.frozenorb.apiv3.utils;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.models.Rank; import net.frozenorb.apiv3.models.Rank;
import java.util.HashMap; import java.util.HashMap;
@ -26,13 +27,11 @@ public class PermissionUtils {
List<String> unconvertedPermissions = unconverted.get(rank.getId()); List<String> unconvertedPermissions = unconverted.get(rank.getId());
// If there's no permissions defined for this rank just skip it. // If there's no permissions defined for this rank just skip it.
if (unconvertedPermissions == null) { if (unconvertedPermissions != null) {
continue; Map<String, Boolean> rankPermissions = convertToMap(unconvertedPermissions);
result = mergePermissions(result, rankPermissions);
} }
Map<String, Boolean> rankPermissions = convertToMap(unconvertedPermissions);
mergePermissions(result, rankPermissions);
if (upTo.getId().equals(rank.getId())) { if (upTo.getId().equals(rank.getId())) {
break; break;
} }