diff --git a/src/main/java/net/frozenorb/apiv3/APIv3.java b/src/main/java/net/frozenorb/apiv3/APIv3.java index bda5fcc..9309e4b 100644 --- a/src/main/java/net/frozenorb/apiv3/APIv3.java +++ b/src/main/java/net/frozenorb/apiv3/APIv3.java @@ -3,15 +3,11 @@ package net.frozenorb.apiv3; import com.google.common.net.MediaType; import com.google.gson.Gson; +import net.frozenorb.apiv3.domain.*; import net.frozenorb.apiv3.web.filter.AuthenticationFilter; import net.frozenorb.apiv3.web.filter.AuthorizationFilter; import net.frozenorb.apiv3.web.filter.MetricsFilter; import net.frozenorb.apiv3.web.filter.WebsiteUserSessionFilter; -import net.frozenorb.apiv3.domain.BannedAsn; -import net.frozenorb.apiv3.domain.BannedCellCarrier; -import net.frozenorb.apiv3.domain.Rank; -import net.frozenorb.apiv3.domain.Server; -import net.frozenorb.apiv3.domain.ServerGroup; import net.frozenorb.apiv3.web.route.GETDumpsType; import net.frozenorb.apiv3.web.route.GETSearch; import net.frozenorb.apiv3.web.route.GETWhoAmI; @@ -56,6 +52,14 @@ import net.frozenorb.apiv3.web.route.notificationTemplates.GETNotificationTempla import net.frozenorb.apiv3.web.route.notificationTemplates.GETNotificationTemplatesId; import net.frozenorb.apiv3.web.route.notificationTemplates.POSTNotificationTemplates; import net.frozenorb.apiv3.web.route.phoneIntel.GETPhoneInteld; +import net.frozenorb.apiv3.web.route.prefix.DELETEPrefixesId; +import net.frozenorb.apiv3.web.route.prefix.GETPrefixes; +import net.frozenorb.apiv3.web.route.prefix.GETPrefixesId; +import net.frozenorb.apiv3.web.route.prefix.POSTPrefixes; +import net.frozenorb.apiv3.web.route.prefixGrants.DELETEPrefixGrantsId; +import net.frozenorb.apiv3.web.route.prefixGrants.GETPrefixGrants; +import net.frozenorb.apiv3.web.route.prefixGrants.GETPrefixGrantsId; +import net.frozenorb.apiv3.web.route.prefixGrants.POSTPrefixGrants; import net.frozenorb.apiv3.web.route.punishments.DELETEPunishmentsId; import net.frozenorb.apiv3.web.route.punishments.DELETEUsersIdActivePunishment; import net.frozenorb.apiv3.web.route.punishments.GETPunishments; @@ -123,6 +127,7 @@ public final class APIv3 { BannedAsn.updateCache(); BannedCellCarrier.updateCache(); Rank.updateCache(); + Prefix.updateCache(); Server.updateCache(); ServerGroup.updateCache(); } @@ -232,6 +237,19 @@ public final class APIv3 { //httpPut(router, "/servers/:serverId", PUTServersId.class); httpDelete(router, "/servers/:serverId", DELETEServersId.class); + + httpGet(router, "/prefixes/:prefixId", GETPrefixesId.class); + httpGet(router, "/prefixes", GETPrefixes.class); + httpPost(router, "/prefixes", POSTPrefixes.class); + //httpPut(router, "/prefixes/:prefixId", PUTPrefixesId.class); + httpDelete(router, "/prefixes/:prefixId", DELETEPrefixesId.class); + + httpGet(router, "/prefixes/grants/:grantId", GETPrefixGrantsId.class); + httpGet(router, "/prefixes/grants", GETPrefixGrants.class); + httpPost(router, "/prefixes/grants", POSTPrefixGrants.class); + //httpPut(router, "/grants/:grantId", PUTPrefixGrantsId.class); + httpDelete(router, "/prefixes/grants/:grantId", DELETEPrefixGrantsId.class); + httpGet(router, "/staff", GETStaff.class); httpGet(router, "/users/:userId", GETUsersId.class); httpGet(router, "/users/:userId/compoundedPermissions", GETUsersIdCompoundedPermissions.class); diff --git a/src/main/java/net/frozenorb/apiv3/domain/Prefix.java b/src/main/java/net/frozenorb/apiv3/domain/Prefix.java new file mode 100644 index 0000000..2b49ef3 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/domain/Prefix.java @@ -0,0 +1,84 @@ +package net.frozenorb.apiv3.domain; + +import com.google.common.collect.ImmutableList; +import com.mongodb.async.SingleResultCallback; +import com.mongodb.async.client.MongoCollection; +import com.mongodb.async.client.MongoDatabase; +import fr.javatic.mongo.jacksonCodec.Entity; +import fr.javatic.mongo.jacksonCodec.objectId.Id; +import io.vertx.core.Vertx; +import lombok.Getter; +import net.frozenorb.apiv3.unsorted.MongoToVoidMongoCallback; +import net.frozenorb.apiv3.util.SpringUtils; +import net.frozenorb.apiv3.util.SyncUtils; +import org.bson.Document; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Entity +public final class Prefix { + + private static final MongoCollection prefixesCollection = SpringUtils.getBean(MongoDatabase.class).getCollection("prefixes", Prefix.class); + + private static Map prefixIdCache = null; + private static List prefixCache = null; + + @Getter @Id private String id; + @Getter private String displayName; + @Getter private String prefix; + @Getter private boolean purchaseable; + @Getter private String buttonName; + @Getter private String buttonDescription; + + public static List findAll() { + return ImmutableList.copyOf(prefixCache); + } + + public static Prefix findById(String id) { + return id == null ? null : prefixIdCache.get(id.toLowerCase()); + } + + static { + SpringUtils.getBean(Vertx.class).setPeriodic(TimeUnit.MINUTES.toMillis(1), (id) -> updateCache()); + } + + public static void updateCache() { + List prefixes = SyncUtils.runBlocking(v -> prefixesCollection.find().into(new LinkedList<>(), v)); + Map working = new HashMap<>(); + + for (Prefix prefix : prefixes) { + working.put(prefix.getId().toLowerCase(), prefix); + } + + prefixIdCache = working; + prefixCache = prefixes; + } + + private Prefix() {} // For Jackson + + public Prefix(String id, String displayName, String prefix, boolean purchaseable, String buttonName, String buttonDescription) { + this.id = id; + this.displayName = displayName; + this.prefix = prefix; + this.purchaseable = purchaseable; + this.buttonName = buttonName; + this.buttonDescription = buttonDescription; + } + + public void insert(SingleResultCallback callback) { + prefixCache.add(this); + prefixIdCache.put(id.toLowerCase(), this); + prefixesCollection.insertOne(this, SyncUtils.vertxWrap(callback)); + } + + public void delete(SingleResultCallback callback) { + prefixCache.remove(this); + prefixIdCache.remove(id.toLowerCase()); + prefixesCollection.deleteOne(new Document("_id", id), SyncUtils.vertxWrap(new MongoToVoidMongoCallback<>(callback))); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/domain/PrefixGrant.java b/src/main/java/net/frozenorb/apiv3/domain/PrefixGrant.java new file mode 100644 index 0000000..452db97 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/domain/PrefixGrant.java @@ -0,0 +1,140 @@ +package net.frozenorb.apiv3.domain; + +import com.google.common.collect.Collections2; +import com.mongodb.async.SingleResultCallback; +import com.mongodb.async.client.MongoCollection; +import com.mongodb.async.client.MongoDatabase; +import fr.javatic.mongo.jacksonCodec.Entity; +import fr.javatic.mongo.jacksonCodec.objectId.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.frozenorb.apiv3.unsorted.MongoToVoidMongoCallback; +import net.frozenorb.apiv3.util.SpringUtils; +import net.frozenorb.apiv3.util.SyncUtils; +import org.bson.Document; +import org.bson.types.ObjectId; + +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +@Entity +@AllArgsConstructor +public final class PrefixGrant { + + private static final MongoCollection grantsCollection = SpringUtils.getBean(MongoDatabase.class).getCollection("prefixGrants", PrefixGrant.class); + + @Getter @Id private String id; + @Getter private UUID user; + @Getter private String reason; + @Getter private Set scopes; + @Getter private String prefix; + @Getter private Instant expiresAt; + + @Getter private UUID addedBy; + @Getter private Instant addedAt; + + @Getter private UUID removedBy; + @Getter private Instant removedAt; + @Getter private String removalReason; + + @Getter private int storeItemId; + @Getter private int storeOrderId; + + public static void findAll(SingleResultCallback> callback) { + grantsCollection.find().sort(new Document("addedAt", -1)).into(new LinkedList<>(), SyncUtils.vertxWrap(callback)); + } + + public static void findByPrefix(Collection prefixes, SingleResultCallback> callback) { + Collection convertedPrefixes = prefixes.stream().map(Prefix::getId).collect(Collectors.toList()); + grantsCollection.find(new Document("prefix", new Document("$in", convertedPrefixes))).into(new LinkedList<>(), SyncUtils.vertxWrap(callback)); + } + + public static void findPaginated(Document query, int skip, int pageSize, SingleResultCallback> callback) { + grantsCollection.find(query).sort(new Document("addedAt", -1)).skip(skip).limit(pageSize).into(new LinkedList<>(), SyncUtils.vertxWrap(callback)); + } + + public static void findById(String id, SingleResultCallback callback) { + grantsCollection.find(new Document("_id", id)).first(SyncUtils.vertxWrap(callback)); + } + + public static void findByUser(User user, SingleResultCallback> callback) { + findByUser(user.getId(), callback); + } + + public static void findByUser(UUID user, SingleResultCallback> callback) { + grantsCollection.find(new Document("user", user)).into(new LinkedList<>(), SyncUtils.vertxWrap(callback)); + } + + public static void findByUserGrouped(Iterable users, SingleResultCallback>> callback) { + grantsCollection.find(new Document("user", new Document("$in", users))).into(new LinkedList<>(), SyncUtils.vertxWrap((grants, error) -> { + if (error != null) { + callback.onResult(null, error); + } else { + Map> result = new HashMap<>(); + + for (UUID user : users) { + result.put(user, new LinkedList<>()); + } + + for (PrefixGrant grant : grants) { + result.get(grant.getUser()).add(grant); + } + + callback.onResult(result, null); + } + })); + } + + private PrefixGrant() {} // For Jackson + + public PrefixGrant(User user, String reason, Set scopes, Prefix prefix, Instant expiresAt, User addedBy) { + this(user, reason, scopes, prefix, expiresAt, addedBy, -1, -1); + } + + public PrefixGrant(User user, String reason, Set scopes, Prefix prefix, Instant expiresAt, User addedBy, int storeItemId, int storeOrderId) { + this.id = new ObjectId().toString(); + this.user = user.getId(); + this.reason = reason; + this.scopes = new HashSet<>(Collections2.transform(scopes, ServerGroup::getId)); + this.prefix = prefix.getId(); + this.expiresAt = expiresAt; + this.addedBy = addedBy == null ? null : addedBy.getId(); + this.addedAt = Instant.now(); + this.storeItemId = storeItemId; + this.storeOrderId = storeOrderId; + } + + public boolean isActive() { + return !(isExpired() || isRemoved()); + } + + public boolean isExpired() { + return expiresAt != null && expiresAt.isBefore(Instant.now()); + } + + public boolean isRemoved() { + return removedAt != null; + } + + public boolean appliesOn(ServerGroup serverGroup) { + return isGlobal() || scopes.contains(serverGroup.getId()); + } + + public boolean isGlobal() { + return scopes.isEmpty(); + } + + public void insert(SingleResultCallback callback) { + grantsCollection.insertOne(this, SyncUtils.vertxWrap(callback)); + } + + public void delete(User removedBy, String reason, SingleResultCallback callback) { + this.removedBy = removedBy == null ? null : removedBy.getId(); + this.removedAt = Instant.now(); + this.removalReason = reason; + + grantsCollection.replaceOne(new Document("_id", id), this, SyncUtils.vertxWrap(new MongoToVoidMongoCallback<>(callback))); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/domain/User.java b/src/main/java/net/frozenorb/apiv3/domain/User.java index 281d7cd..9efd93f 100644 --- a/src/main/java/net/frozenorb/apiv3/domain/User.java +++ b/src/main/java/net/frozenorb/apiv3/domain/User.java @@ -690,14 +690,16 @@ public final class User { } Rank parent = otherRank; + if(parent != null) { - // Iterate up the inheritance tree to detect rank redundancies. - while (parent.getInheritsFromId() != null) { - if (parent == grantedRank) { - grantedRanks.remove(grantedRank); + // Iterate up the inheritance tree to detect rank redundancies. + while (parent.getInheritsFromId() != null) { + if (parent == grantedRank) { + grantedRanks.remove(grantedRank); + } + + parent = Rank.findById(parent.getInheritsFromId()); } - - parent = Rank.findById(parent.getInheritsFromId()); } } } diff --git a/src/main/java/net/frozenorb/apiv3/service/auditlog/AuditLogActionType.java b/src/main/java/net/frozenorb/apiv3/service/auditlog/AuditLogActionType.java index e88672c..ed005c4 100644 --- a/src/main/java/net/frozenorb/apiv3/service/auditlog/AuditLogActionType.java +++ b/src/main/java/net/frozenorb/apiv3/service/auditlog/AuditLogActionType.java @@ -27,6 +27,10 @@ public enum AuditLogActionType { GRANT_CREATE(false), GRANT_UPDATE(false), GRANT_DELETE(false), + PREFIXGRANT_CREATE(false), + PREFIXGRANT_UPDATE(false), + PREFIXGRANT_DELETE(false), + IP_BAN_CREATE(false), IP_BAN_UPDATE(false), IP_BAN_DELETE(false), @@ -62,6 +66,11 @@ public enum AuditLogActionType { RANK_CREATE(false), RANK_UPDATE(false), RANK_DELETE(false), + PREFIX_CREATE(false), + PREFIX_UPDATE(false), + PREFIX_DELETE(false), + + SERVER_GROUP_CREATE(false), SERVER_GROUP_UPDATE(false), SERVER_GROUP_DELETE(false), diff --git a/src/main/java/net/frozenorb/apiv3/unsorted/Permissions.java b/src/main/java/net/frozenorb/apiv3/unsorted/Permissions.java index 11c5b80..2ec4198 100644 --- a/src/main/java/net/frozenorb/apiv3/unsorted/Permissions.java +++ b/src/main/java/net/frozenorb/apiv3/unsorted/Permissions.java @@ -17,4 +17,6 @@ public class Permissions { public static final String CREATE_GRANT = rootPermission + ".grant.create"; public static final String REMOVE_GRANT = rootPermission + ".grant.remove"; + public static final String CREATE_PREFIXGRANT = rootPermission + ".prefixgrant.create"; + public static final String REMOVE_PREFIXGRANT = rootPermission + ".prefixgrant.remove"; } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/unsorted/actor/Actor.java b/src/main/java/net/frozenorb/apiv3/unsorted/actor/Actor.java index 0d43b39..3570a53 100644 --- a/src/main/java/net/frozenorb/apiv3/unsorted/actor/Actor.java +++ b/src/main/java/net/frozenorb/apiv3/unsorted/actor/Actor.java @@ -1,5 +1,7 @@ package net.frozenorb.apiv3.unsorted.actor; +import com.google.common.base.MoreObjects; + public interface Actor { boolean isAuthorized(); @@ -8,4 +10,5 @@ public interface Actor { ActorType getType(); + } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/unsorted/actor/SimpleActor.java b/src/main/java/net/frozenorb/apiv3/unsorted/actor/SimpleActor.java index 1d74e2c..341f19a 100644 --- a/src/main/java/net/frozenorb/apiv3/unsorted/actor/SimpleActor.java +++ b/src/main/java/net/frozenorb/apiv3/unsorted/actor/SimpleActor.java @@ -1,5 +1,6 @@ package net.frozenorb.apiv3.unsorted.actor; +import com.google.common.base.MoreObjects; import lombok.AllArgsConstructor; import lombok.Getter; @@ -9,5 +10,12 @@ public final class SimpleActor implements Actor { @Getter private String name; @Getter private ActorType type; @Getter private boolean authorized; - + @Override + public String toString() { + return MoreObjects.toStringHelper(getClass()) + .add("authorized", isAuthorized()) + .add("name",getName()) + .add("type",getType().name()) + .toString(); + } } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/util/PermissionUtils.java b/src/main/java/net/frozenorb/apiv3/util/PermissionUtils.java index b3a89f2..3ccf161 100644 --- a/src/main/java/net/frozenorb/apiv3/util/PermissionUtils.java +++ b/src/main/java/net/frozenorb/apiv3/util/PermissionUtils.java @@ -35,7 +35,6 @@ public class PermissionUtils { for (Rank rank : mergeQueue) { Map rankPermissions = convertFromList(raw.get(rank.getId())); - // If there's no permissions defined for this rank just skip it. if (!rankPermissions.isEmpty()) { result = mergePermissions(result, rankPermissions); diff --git a/src/main/java/net/frozenorb/apiv3/web/filter/AuthorizationFilter.java b/src/main/java/net/frozenorb/apiv3/web/filter/AuthorizationFilter.java index 243e902..5de9834 100644 --- a/src/main/java/net/frozenorb/apiv3/web/filter/AuthorizationFilter.java +++ b/src/main/java/net/frozenorb/apiv3/web/filter/AuthorizationFilter.java @@ -18,11 +18,11 @@ public final class AuthorizationFilter implements Handler { Actor actor = ctx.get("actor"); ctx.next(); - /*if (actor.isAuthorized()) { + if (actor.isAuthorized()) { } else { ErrorUtils.respondOther(ctx, 403, "Failed to authorize as an approved actor.", "failedToAuthorizeNotApprovedActor", ImmutableMap.of()); - }*/ + } } } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/web/route/prefix/DELETEPrefixesId.java b/src/main/java/net/frozenorb/apiv3/web/route/prefix/DELETEPrefixesId.java new file mode 100644 index 0000000..77c55dc --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/web/route/prefix/DELETEPrefixesId.java @@ -0,0 +1,44 @@ +package net.frozenorb.apiv3.web.route.prefix; + +import com.google.common.collect.ImmutableMap; +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.domain.Prefix; +import net.frozenorb.apiv3.service.auditlog.AuditLog; +import net.frozenorb.apiv3.service.auditlog.AuditLogActionType; +import net.frozenorb.apiv3.util.ErrorUtils; +import net.frozenorb.apiv3.util.SyncUtils; +import net.frozenorb.apiv3.util.UuidUtils; +import org.springframework.stereotype.Component; + +@Component +public final class DELETEPrefixesId implements Handler { + + public void handle(RoutingContext ctx) { + Prefix prefix = Prefix.findById(ctx.request().getParam("prefixId")); + + if (prefix == null) { + ErrorUtils.respondNotFound(ctx, "Prefix", ctx.request().getParam("prefixId")); + return; + } + + SyncUtils.runBlocking(v -> prefix.delete(v)); + + JsonObject requestBody = ctx.getBodyAsJson(); + + if (requestBody.containsKey("removedBy")) { + AuditLog.log(UuidUtils.parseUuid(requestBody.getString("removedBy")), requestBody.getString("removedByIp"), ctx, AuditLogActionType.RANK_DELETE, ImmutableMap.of("prefixId", prefix.getId()), (ignored, error) -> { + if (error != null) { + ErrorUtils.respondInternalError(ctx, error); + } else { + APIv3.respondJson(ctx, 200, prefix); + } + }); + } else { + APIv3.respondJson(ctx, 200, prefix); + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/web/route/prefix/GETPrefixes.java b/src/main/java/net/frozenorb/apiv3/web/route/prefix/GETPrefixes.java new file mode 100644 index 0000000..ddb5a24 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/web/route/prefix/GETPrefixes.java @@ -0,0 +1,19 @@ +package net.frozenorb.apiv3.web.route.prefix; + +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.domain.Prefix; +import net.frozenorb.apiv3.domain.Rank; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; + +@Component +public final class GETPrefixes implements Handler { + + public void handle(RoutingContext ctx) { + APIv3.respondJson(ctx, 200, Prefix.findAll()); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/web/route/prefix/GETPrefixesId.java b/src/main/java/net/frozenorb/apiv3/web/route/prefix/GETPrefixesId.java new file mode 100644 index 0000000..b6cd3eb --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/web/route/prefix/GETPrefixesId.java @@ -0,0 +1,16 @@ +package net.frozenorb.apiv3.web.route.prefix; + +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.domain.Prefix; +import org.springframework.stereotype.Component; + +@Component +public final class GETPrefixesId implements Handler { + + public void handle(RoutingContext ctx) { + APIv3.respondJson(ctx, 200, Prefix.findById(ctx.request().getParam("prefixId"))); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/web/route/prefix/POSTPrefixes.java b/src/main/java/net/frozenorb/apiv3/web/route/prefix/POSTPrefixes.java new file mode 100644 index 0000000..9edd2e3 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/web/route/prefix/POSTPrefixes.java @@ -0,0 +1,44 @@ +package net.frozenorb.apiv3.web.route.prefix; + +import com.google.common.collect.ImmutableMap; +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.domain.Prefix; +import net.frozenorb.apiv3.service.auditlog.AuditLog; +import net.frozenorb.apiv3.service.auditlog.AuditLogActionType; +import net.frozenorb.apiv3.util.ErrorUtils; +import net.frozenorb.apiv3.util.SyncUtils; +import net.frozenorb.apiv3.util.UuidUtils; +import org.springframework.stereotype.Component; + +@Component +public final class POSTPrefixes implements Handler { + + public void handle(RoutingContext ctx) { + JsonObject requestBody = ctx.getBodyAsJson(); + String id = requestBody.getString("id"); + String displayName = requestBody.getString("displayName"); + String prefix = requestBody.getString("prefix"); + boolean purchaseable = requestBody.getBoolean("purchaseable"); + String buttonName = requestBody.getString("buttonName"); + String buttonDescription = requestBody.getString("buttonDescription"); + + Prefix pref = new Prefix(id, displayName, prefix, purchaseable, buttonName, buttonDescription); + SyncUtils.runBlocking(pref::insert); + + if (requestBody.containsKey("addedBy")) { + AuditLog.log(UuidUtils.parseUuid(requestBody.getString("addedBy")), requestBody.getString("addedByIp"), ctx, AuditLogActionType.PREFIX_CREATE, ImmutableMap.of("prefixId", id), (ignored, error) -> { + if (error != null) { + ErrorUtils.respondInternalError(ctx, error); + } else { + APIv3.respondJson(ctx, 200, pref); + } + }); + } else { + APIv3.respondJson(ctx, 200, pref); + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/web/route/prefix/PUTPrefixesId.java b/src/main/java/net/frozenorb/apiv3/web/route/prefix/PUTPrefixesId.java new file mode 100644 index 0000000..19372b8 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/web/route/prefix/PUTPrefixesId.java @@ -0,0 +1,7 @@ +package net.frozenorb.apiv3.web.route.prefix; + +import org.springframework.stereotype.Component; + +@Component +public class PUTPrefixesId { +} diff --git a/src/main/java/net/frozenorb/apiv3/web/route/prefixGrants/DELETEPrefixGrantsId.java b/src/main/java/net/frozenorb/apiv3/web/route/prefixGrants/DELETEPrefixGrantsId.java new file mode 100644 index 0000000..9466cd8 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/web/route/prefixGrants/DELETEPrefixGrantsId.java @@ -0,0 +1,65 @@ +package net.frozenorb.apiv3.web.route.prefixGrants; + +import com.google.common.collect.ImmutableMap; +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.domain.Grant; +import net.frozenorb.apiv3.domain.User; +import net.frozenorb.apiv3.service.auditlog.AuditLog; +import net.frozenorb.apiv3.service.auditlog.AuditLogActionType; +import net.frozenorb.apiv3.unsorted.Permissions; +import net.frozenorb.apiv3.util.ErrorUtils; +import net.frozenorb.apiv3.util.SyncUtils; +import org.springframework.stereotype.Component; + +@Component +public final class DELETEPrefixGrantsId implements Handler { + + public void handle(RoutingContext ctx) { + Grant grant = SyncUtils.runBlocking(v -> Grant.findById(ctx.request().getParam("grantId"), v)); + + if (grant == null) { + ErrorUtils.respondNotFound(ctx, "Grant", ctx.request().getParam("grantId")); + return; + } else if (!grant.isActive()) { + ErrorUtils.respondInvalidInput(ctx, "Cannot remove an inactive grant."); + return; + } + + JsonObject requestBody = ctx.getBodyAsJson(); + // We purposely don't do a null check, grant removals don't have to have a user/ip. + User removedBy = SyncUtils.runBlocking(v -> User.findById(requestBody.getString("removedBy"), v)); + String reason = requestBody.getString("reason"); + + if (reason == null || reason.trim().isEmpty()) { + ErrorUtils.respondRequiredInput(ctx, "reason"); + return; + } + + if (removedBy != null) { + boolean allowed = SyncUtils.runBlocking(v -> removedBy.hasPermissionAnywhere(Permissions.REMOVE_PREFIXGRANT + "." + grant.getRank(), v)); + + if (!allowed) { + ErrorUtils.respondOther(ctx, 409, "User given does not have permission to remove this grant.", "userDoesNotHavePermission", ImmutableMap.of()); + return; + } + } + + SyncUtils.runBlocking(v -> grant.delete(removedBy, reason, v)); + + if (removedBy != null) { + AuditLog.log(removedBy.getId(), requestBody.getString("removedByIp"), ctx, AuditLogActionType.PREFIXGRANT_DELETE, ImmutableMap.of("grantId", grant.getId()), (ignored, error) -> { + if (error != null) { + ErrorUtils.respondInternalError(ctx, error); + } else { + APIv3.respondJson(ctx, 200, grant); + } + }); + } else { + APIv3.respondJson(ctx, 200, grant); + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/web/route/prefixGrants/GETPrefixGrants.java b/src/main/java/net/frozenorb/apiv3/web/route/prefixGrants/GETPrefixGrants.java new file mode 100644 index 0000000..b800678 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/web/route/prefixGrants/GETPrefixGrants.java @@ -0,0 +1,37 @@ +package net.frozenorb.apiv3.web.route.prefixGrants; + +import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.domain.PrefixGrant; +import net.frozenorb.apiv3.util.ErrorUtils; +import net.frozenorb.apiv3.util.UuidUtils; + +import org.bson.Document; +import org.springframework.stereotype.Component; + +import java.util.stream.Collectors; + +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +@Component +public final class GETPrefixGrants implements Handler { + + public void handle(RoutingContext ctx) { + try { + int skip = ctx.request().getParam("skip") == null ? 0 : Integer.parseInt(ctx.request().getParam("skip")); + int pageSize = ctx.request().getParam("pageSize") == null ? 100 : Integer.parseInt(ctx.request().getParam("pageSize")); + + PrefixGrant.findPaginated(ctx.request().getParam("user") == null ? new Document() : new Document("user", UuidUtils.parseUuid(ctx.request().getParam("user"))), skip, pageSize, (grants, error) -> { + if (ctx.request().getParam("active") != null) { + boolean requireActive = Boolean.parseBoolean(ctx.request().getParam("active")); + APIv3.respondJson(ctx, 200, grants.stream().filter(grant -> grant.isActive() == requireActive).collect(Collectors.toList())); + } else { + APIv3.respondJson(ctx, 200, grants); + } + }); + } catch (NumberFormatException ignored) { + ErrorUtils.respondInvalidInput(ctx, "skip and pageSize must be numerical inputs."); + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/web/route/prefixGrants/GETPrefixGrantsId.java b/src/main/java/net/frozenorb/apiv3/web/route/prefixGrants/GETPrefixGrantsId.java new file mode 100644 index 0000000..fcfcae4 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/web/route/prefixGrants/GETPrefixGrantsId.java @@ -0,0 +1,24 @@ +package net.frozenorb.apiv3.web.route.prefixGrants; + +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.domain.Grant; +import net.frozenorb.apiv3.domain.PrefixGrant; +import net.frozenorb.apiv3.util.ErrorUtils; +import org.springframework.stereotype.Component; + +@Component +public final class GETPrefixGrantsId implements Handler { + + public void handle(RoutingContext ctx) { + PrefixGrant.findById(ctx.request().getParam("grantId"), (grant, error) -> { + if (error != null) { + ErrorUtils.respondInternalError(ctx, error); + } else { + APIv3.respondJson(ctx, 200, grant); + } + }); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/web/route/prefixGrants/POSTPrefixGrants.java b/src/main/java/net/frozenorb/apiv3/web/route/prefixGrants/POSTPrefixGrants.java new file mode 100644 index 0000000..99fddd7 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/web/route/prefixGrants/POSTPrefixGrants.java @@ -0,0 +1,125 @@ +package net.frozenorb.apiv3.web.route.prefixGrants; + +import com.google.common.collect.ImmutableMap; +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.domain.*; +import net.frozenorb.apiv3.service.auditlog.AuditLog; +import net.frozenorb.apiv3.service.auditlog.AuditLogActionType; +import net.frozenorb.apiv3.service.totp.TotpAuthorizationResult; +import net.frozenorb.apiv3.unsorted.Permissions; +import net.frozenorb.apiv3.util.ErrorUtils; +import net.frozenorb.apiv3.util.SyncUtils; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Component +public final class POSTPrefixGrants implements Handler { + + public void handle(RoutingContext ctx) { + JsonObject requestBody = ctx.getBodyAsJson(); + User target = SyncUtils.runBlocking(v -> User.findById(requestBody.getString("user"), v)); + + if (target == null) { + ErrorUtils.respondNotFound(ctx, "User", requestBody.getString("user")); + return; + } + + String reason = requestBody.getString("reason"); + + if (reason == null || reason.trim().isEmpty()) { + ErrorUtils.respondRequiredInput(ctx, "reason"); + return; + } + + Set scopes = new HashSet<>(); + List scopeIds = (List) requestBody.getJsonArray("scopes").getList(); + + if (!scopeIds.isEmpty()) { + for (String serverGroupId : scopeIds) { + ServerGroup serverGroup = ServerGroup.findById(serverGroupId); + + if (serverGroup == null) { + ErrorUtils.respondNotFound(ctx, "Server group", serverGroupId); + return; + } + + scopes.add(serverGroup); + } + } + + Prefix prefix = Prefix.findById(requestBody.getString("prefix")); + + if (prefix == null) { + ErrorUtils.respondNotFound(ctx, "Prefix", requestBody.getString("prefix")); + return; + } + + Instant expiresAt = null; + + if (requestBody.containsKey("expiresIn") && requestBody.getLong("expiresIn") != -1) { + long expiresInMillis = requestBody.getLong("expiresIn") * 1000; + expiresAt = Instant.ofEpochMilli(System.currentTimeMillis() + expiresInMillis); + } + + if (expiresAt != null && expiresAt.isBefore(Instant.now())) { + ErrorUtils.respondInvalidInput(ctx, "Expiration time cannot be in the past."); + return; + } + + // We purposely don't fail on a null check, grants don't have to have a source. + User addedBy = SyncUtils.runBlocking(v -> User.findById(requestBody.getString("addedBy"), v)); + + if (addedBy != null) { + boolean allowed = SyncUtils.runBlocking(v -> addedBy.hasPermissionAnywhere(Permissions.CREATE_PREFIXGRANT + "." + prefix.getId(), v)); + + if (!allowed) { + ErrorUtils.respondOther(ctx, 409, "User given does not have permission to create this grant.", "userDoesNotHavePermission", ImmutableMap.of()); + return; + } + + if (false) { + if (!requestBody.containsKey("totpCode")) { + ErrorUtils.respondInvalidInput(ctx, "prefix must be granted through API or website."); + return; + } + + int code = requestBody.getInteger("totpCode", -1); + TotpAuthorizationResult totpAuthorizationResult = SyncUtils.runBlocking(v -> addedBy.checkTotpAuthorization(code, null, v)); + + if (!totpAuthorizationResult.isAuthorized()) { + ErrorUtils.respondInvalidInput(ctx, "Totp authorization failed: " + totpAuthorizationResult.name()); + return; + } + } + } else if (false) { + ErrorUtils.respondInvalidInput(ctx, "Prefix must be granted through API or website."); + return; + } + + int storeItemId = requestBody.getInteger("storeItemId", -1); + int storeOrderId = requestBody.getInteger("storeOrderId", -1); + + PrefixGrant grant = new PrefixGrant(target, reason, scopes, prefix, expiresAt, addedBy, storeItemId, storeOrderId); + SyncUtils.runBlocking(v -> grant.insert(v)); + + if (addedBy != null) { + AuditLog.log(addedBy.getId(), requestBody.getString("addedByIp"), ctx, AuditLogActionType.PREFIXGRANT_CREATE, ImmutableMap.of("grantId", grant.getId()), (ignored, error) -> { + if (error != null) { + ErrorUtils.respondInternalError(ctx, error); + } else { + APIv3.respondJson(ctx, 200, grant); + } + }); + } else { + APIv3.respondJson(ctx, 200, grant); + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/web/route/servers/POSTServersHeartbeat.java b/src/main/java/net/frozenorb/apiv3/web/route/servers/POSTServersHeartbeat.java index 556af74..666173c 100644 --- a/src/main/java/net/frozenorb/apiv3/web/route/servers/POSTServersHeartbeat.java +++ b/src/main/java/net/frozenorb/apiv3/web/route/servers/POSTServersHeartbeat.java @@ -40,15 +40,16 @@ public final class POSTServersHeartbeat implements Handler { public void handle(RoutingContext ctx) { Actor actor = ctx.get("actor"); - if (actor.getType() != ActorType.SERVER) { - ErrorUtils.respondOther(ctx, 403, "This action can only be performed when requested by a server.", "serverOnly", ImmutableMap.of()); + ErrorUtils.respondOther(ctx, 403, + "This action can only be performed when requested by a server.", "serverOnly", ImmutableMap.of()); return; } Server actorServer = Server.findById(actor.getName()); + JsonObject requestBody = ctx.getBodyAsJson(); - System.out.println("body: " + requestBody); + JsonObject players = requestBody.getJsonObject("players"); Map playerNames = extractPlayerNames(players); Map playerIps = extractPlayerIps(players); @@ -75,6 +76,7 @@ public final class POSTServersHeartbeat implements Handler { } private Future createInfoResponse(Server server, double tps, Map playerNames) { + Future callback = Future.future(); if (server != null && playerNames != null) { @@ -92,6 +94,7 @@ public final class POSTServersHeartbeat implements Handler { } private Future> createPlayerResponse(Server server, Map playerNames, Map playerIps) { + Future> callback = Future.future(); Future> userLookupCallback = Future.future(); @@ -139,18 +142,19 @@ public final class POSTServersHeartbeat implements Handler { } private Future> createPermissionsResponse(ServerGroup serverGroup) { + Future> callback = Future.future(); Map permissionsResponse = new HashMap<>(); for (Rank rank : Rank.findAll()) { + Map defaultCalculatedPermissions = ServerGroup.findDefault().calculatePermissions(rank); + Map calculatedPermissions = serverGroup.calculatePermissions(rank); Map scopedPermissions = PermissionUtils.mergePermissions( - ServerGroup.findDefault().calculatePermissions(rank), - serverGroup.calculatePermissions(rank) + defaultCalculatedPermissions, + calculatedPermissions ); - permissionsResponse.put(rank.getId(), PermissionUtils.convertToList(scopedPermissions)); } - callback.complete(permissionsResponse); return callback; }