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.GETNotificationTemplates;
import net.frozenorb.apiv3.routes.notificationTemplate.POSTNotificationTemplate;
import net.frozenorb.apiv3.routes.punishments.DELETEPunishment;
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.punishments.*;
import net.frozenorb.apiv3.routes.ranks.DELETERank;
import net.frozenorb.apiv3.routes.ranks.GETRank;
import net.frozenorb.apiv3.routes.ranks.GETRanks;
@ -146,12 +143,14 @@ public final class APIv3 {
private void setupHttp() {
ipAddress(config.getProperty("http.address"));
port(Integer.parseInt(config.getProperty("http.port")));
// TODO: if threadPool == null use default value
threadPool(Integer.parseInt(config.getProperty("http.workerThreads")));
before(new ContentTypeFilter());
before(new ActorAttributeFilter());
before(new AuthorizationFilter());
before(new MetricsBeforeFilter());
after(new MetricsAfterFilter());
after(new LoggingFilter());
exception(Exception.class, new LoggingExceptionHandler());
// TODO: The commented out routes
@ -200,13 +199,14 @@ public final class APIv3 {
get("/user/:id/details", new GETUserDetails(), gson::toJson);
get("/user/:id/meta/:serverGroup", new GETUserMeta(), 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/requiresTOTP", new GETUserRequiresTOTP(), gson::toJson);
get("/user/:id/verifyPassword", new GETUserVerifyPassword(), gson::toJson);
get("/user/:id", new GETUser(), gson::toJson);
post("/user/:id/verifyTOTP", new POSTUserVerifyTOTP(), gson::toJson);
post("/user/:id:/grant", new POSTUserGrant(), gson::toJson);
post("/user/:id:/punish", new POSTUserPunish(), gson::toJson);
post("/user/:id/grant", new POSTUserGrant(), gson::toJson);
post("/user/:id/punish", new POSTUserPunish(), gson::toJson);
post("/user/:id/login", new POSTUserLogin(), gson::toJson);
post("/user/:id/notify", new POSTUserNotify(), 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);
put("/user/:id/meta/:serverGroup", new PUTUserMeta(), 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 :(
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.Response;
import java.util.concurrent.TimeUnit;
public final class MetricsAfterFilter implements Filter {
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) {
responseLengthMetric.update(req.contentLength());
Timer.Context timerMetric = req.attribute("timerMetric");
timerMetric.stop();
long started = req.attribute("requestStarted");
responseTimesMetric.update(System.currentTimeMillis() - started, TimeUnit.MILLISECONDS);
}
}

View File

@ -9,10 +9,8 @@ import spark.Response;
public final class MetricsBeforeFilter implements Filter {
private Timer responseTimesMetric = APIv3.getMetrics().timer(MetricRegistry.name("apiv3", "http", "responseTimes"));
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 @Indexed private UUID target;
@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 private Date expiresAt;
@ -42,8 +42,8 @@ public final class Grant {
this.reason = reason;
this.scopes = new HashSet<>(Collections2.transform(scopes, ServerGroup::getId));
this.rank = rank.getId();
this.expiresAt = (Date) expiresAt.clone();
this.addedBy = addedBy.getId();
this.expiresAt = expiresAt;
this.addedBy = addedBy == null ? null : addedBy.getId();
this.addedAt = new Date();
}
@ -63,7 +63,7 @@ public final class Grant {
if (expiresAt == null) {
return false; // Never expires
} 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.reason = reason;
this.type = type;
this.expiresAt = (Date) expiresAt.clone();
this.addedBy = addedBy.getId();
this.expiresAt = expiresAt;
this.addedBy = addedBy == null ? null : addedBy.getId();
this.addedAt = new Date();
this.actorName = actor.getName();
this.actorType = actor.getType();
@ -64,7 +64,7 @@ public final class Punishment {
if (expiresAt == null) {
return false; // Never expires
} 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 org.mongodb.morphia.annotations.Entity;
import org.mongodb.morphia.annotations.Id;
import org.mongodb.morphia.annotations.Property;
import java.util.*;
@ -14,7 +15,8 @@ import java.util.*;
public final class ServerGroup {
@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
// empty, Morphia will load them as null, not empty sets.
@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 lastSeenOn;
@Getter private Date lastSeenAt;
@Getter private Date firstSeen;
@Getter private Date firstSeenAt;
public static User byId(String id) {
try {
@ -65,7 +65,7 @@ public final class User {
this.phoneNumber = null;
this.lastSeenOn = null;
this.lastSeenAt = new Date();
this.firstSeen = new Date();
this.firstSeenAt = new Date();
aliases.put(lastUsername, new Date());
}
@ -157,7 +157,7 @@ public final class User {
}
public Rank getHighestRank(ServerGroup serverGroup) {
Rank highest = null;
Rank highest = null;;
for (Grant grant : getGrants()) {
if (!grant.isActive() || (serverGroup != null && !grant.appliesOn(serverGroup))) {
@ -231,6 +231,7 @@ public final class User {
ServerGroup actorGroup = ServerGroup.byId(server.getGroup());
Rank highestRank = getHighestRank(actorGroup);
Map<String, Boolean> scopedPermissions = PermissionUtils.mergePermissions(
PermissionUtils.getDefaultPermissions(highestRank),
actorGroup.calculatePermissions(highestRank)

View File

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

View File

@ -31,8 +31,10 @@ public final class POSTUserGrant implements Route {
}
Set<ServerGroup> scopes = new HashSet<>();
String scopesUnparsed = req.queryParams("scopes");
for (String serverGroupId : req.queryParams("scopes").split(",")) {
if (!scopesUnparsed.isEmpty()) {
for (String serverGroupId : scopesUnparsed.split(",")) {
ServerGroup serverGroup = ServerGroup.byId(serverGroupId);
if (serverGroup == null) {
@ -41,6 +43,7 @@ public final class POSTUserGrant implements Route {
scopes.add(serverGroup);
}
}
Rank rank = Rank.byId(req.queryParams("rank"));
@ -48,20 +51,20 @@ public final class POSTUserGrant implements Route {
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.");
}
// We purposely don't do a null check, grants don't have to have a source.
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);
APIv3.getDatastore().save(grant);

View File

@ -23,12 +23,9 @@ public final class DELETEPunishment implements Route {
}
User removedBy = User.byId(req.queryParams("removedBy"));
String requiredPermission = Permissions.REMOVE_PUNISHMENT + "." + punishment.getType().name();
if (removedBy == null) {
return ErrorUtils.notFound("User", req.queryParams("removedBy"));
} else if (!removedBy.hasPermissionAnywhere(requiredPermission)) {
return ErrorUtils.unauthorized(requiredPermission);
}
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;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
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.User;
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"));
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.");
}
// We purposely don't do a null check, grants don't have to have a source.
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)) {
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 -> {
if (grant.isActive()) {
User user = User.byId(grant.getTarget());
Rank rank = staffRanks.get(grant.getRank());
if (!result.containsKey(rank)) {
result.put(rank, new HashSet<>());
if (!result.containsKey(rank.getId())) {
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("punishments", user.getPunishments())
.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 lombok.experimental.UtilityClass;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.models.Rank;
import java.util.HashMap;
@ -26,12 +27,10 @@ public class PermissionUtils {
List<String> unconvertedPermissions = unconverted.get(rank.getId());
// If there's no permissions defined for this rank just skip it.
if (unconvertedPermissions == null) {
continue;
}
if (unconvertedPermissions != null) {
Map<String, Boolean> rankPermissions = convertToMap(unconvertedPermissions);
mergePermissions(result, rankPermissions);
result = mergePermissions(result, rankPermissions);
}
if (upTo.getId().equals(rank.getId())) {
break;