mirror of
https://github.com/lucko/LuckPerms.git
synced 2025-08-13 18:14:00 +02:00
Add REST storage backend (#3939)
This commit is contained in:
@@ -16,6 +16,7 @@ jacocoTestReport {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'org.slf4j:slf4j-simple:1.7.36'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.1'
|
||||
@@ -27,6 +28,7 @@ dependencies {
|
||||
testImplementation 'org.spongepowered:configurate-yaml:3.7.2'
|
||||
testImplementation 'org.spongepowered:configurate-hocon:3.7.2'
|
||||
testImplementation 'me.lucko.configurate:configurate-toml:3.7'
|
||||
testImplementation 'net.luckperms:rest-api-java-client:0.1-SNAPSHOT'
|
||||
|
||||
api project(':api')
|
||||
api 'org.checkerframework:checker-qual:3.12.0'
|
||||
@@ -99,4 +101,5 @@ dependencies {
|
||||
compileOnly 'org.mongodb:mongodb-driver-legacy:4.5.0'
|
||||
compileOnly 'org.postgresql:postgresql:42.6.0'
|
||||
compileOnly 'org.yaml:snakeyaml:1.28'
|
||||
compileOnly 'net.luckperms:rest-api-java-client:0.1-SNAPSHOT'
|
||||
}
|
||||
|
@@ -605,6 +605,16 @@ public final class ConfigKeys {
|
||||
return c.getString("data.mongodb-connection-uri", c.getString("data.mongodb_connection_URI", ""));
|
||||
}));
|
||||
|
||||
/**
|
||||
* The REST storage URL
|
||||
*/
|
||||
public static final ConfigKey<String> REST_STORAGE_URL = notReloadable(stringKey("data.rest-url", "http://localhost:8080/"));
|
||||
|
||||
/**
|
||||
* The REST storage auth key
|
||||
*/
|
||||
public static final ConfigKey<String> REST_STORAGE_AUTH_KEY = notReloadable(stringKey("data.rest-auth-key", ""));
|
||||
|
||||
/**
|
||||
* The name of the storage method being used
|
||||
*/
|
||||
|
@@ -72,7 +72,7 @@ public final class StandardNodeMatchers {
|
||||
return new TypeEquals<>(type);
|
||||
}
|
||||
|
||||
private static class Generic extends ConstraintNodeMatcher<Node> {
|
||||
public static class Generic extends ConstraintNodeMatcher<Node> {
|
||||
Generic(Comparison comparison, String value) {
|
||||
super(comparison, value);
|
||||
}
|
||||
@@ -83,7 +83,7 @@ public final class StandardNodeMatchers {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NodeEquals<T extends Node> extends ConstraintNodeMatcher<T> {
|
||||
public static final class NodeEquals<T extends Node> extends ConstraintNodeMatcher<T> {
|
||||
private final T node;
|
||||
private final NodeEqualityPredicate equalityPredicate;
|
||||
|
||||
@@ -93,6 +93,14 @@ public final class StandardNodeMatchers {
|
||||
this.equalityPredicate = equalityPredicate;
|
||||
}
|
||||
|
||||
public T getNode() {
|
||||
return this.node;
|
||||
}
|
||||
|
||||
public NodeEqualityPredicate getEqualityPredicate() {
|
||||
return this.equalityPredicate;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public @Nullable T filterConstraintMatch(@NonNull Node node) {
|
||||
@@ -103,9 +111,16 @@ public final class StandardNodeMatchers {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MetaKeyEquals extends ConstraintNodeMatcher<MetaNode> {
|
||||
public static final class MetaKeyEquals extends ConstraintNodeMatcher<MetaNode> {
|
||||
private final String metaKey;
|
||||
|
||||
MetaKeyEquals(String metaKey) {
|
||||
super(Comparison.SIMILAR, Meta.key(metaKey, Comparison.WILDCARD));
|
||||
this.metaKey = metaKey;
|
||||
}
|
||||
|
||||
public String getMetaKey() {
|
||||
return this.metaKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -114,7 +129,7 @@ public final class StandardNodeMatchers {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TypeEquals<T extends Node> extends ConstraintNodeMatcher<T> {
|
||||
public static final class TypeEquals<T extends Node> extends ConstraintNodeMatcher<T> {
|
||||
private final NodeType<? extends T> type;
|
||||
|
||||
TypeEquals(NodeType<? extends T> type) {
|
||||
@@ -122,6 +137,10 @@ public final class StandardNodeMatchers {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public NodeType<? extends T> getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable T filterConstraintMatch(@NonNull Node node) {
|
||||
return this.type.tryCast(node).orElse(null);
|
||||
|
@@ -37,6 +37,7 @@ import me.lucko.luckperms.common.storage.implementation.file.loader.JsonLoader;
|
||||
import me.lucko.luckperms.common.storage.implementation.file.loader.TomlLoader;
|
||||
import me.lucko.luckperms.common.storage.implementation.file.loader.YamlLoader;
|
||||
import me.lucko.luckperms.common.storage.implementation.mongodb.MongoStorage;
|
||||
import me.lucko.luckperms.common.storage.implementation.rest.RestStorage;
|
||||
import me.lucko.luckperms.common.storage.implementation.split.SplitStorage;
|
||||
import me.lucko.luckperms.common.storage.implementation.split.SplitStorageType;
|
||||
import me.lucko.luckperms.common.storage.implementation.sql.SqlStorage;
|
||||
@@ -129,6 +130,12 @@ public class StorageFactory {
|
||||
this.plugin.getConfiguration().get(ConfigKeys.MONGODB_COLLECTION_PREFIX),
|
||||
this.plugin.getConfiguration().get(ConfigKeys.MONGODB_CONNECTION_URI)
|
||||
);
|
||||
case REST:
|
||||
return new RestStorage(
|
||||
this.plugin,
|
||||
this.plugin.getConfiguration().get(ConfigKeys.REST_STORAGE_URL),
|
||||
this.plugin.getConfiguration().get(ConfigKeys.REST_STORAGE_AUTH_KEY)
|
||||
);
|
||||
case YAML:
|
||||
return new SeparatedConfigurateStorage(this.plugin, "YAML", new YamlLoader(), ".yml", "yaml-storage");
|
||||
case JSON:
|
||||
|
@@ -51,6 +51,9 @@ public enum StorageType {
|
||||
SQLITE("SQLite", "sqlite"),
|
||||
H2("H2", "h2"),
|
||||
|
||||
// REST
|
||||
REST("REST", "rest"),
|
||||
|
||||
// Custom
|
||||
CUSTOM("Custom", "custom");
|
||||
|
||||
|
@@ -0,0 +1,664 @@
|
||||
/*
|
||||
* This file is part of LuckPerms, licensed under the MIT License.
|
||||
*
|
||||
* Copyright (c) lucko (Luck) <luck@lucko.me>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package me.lucko.luckperms.common.storage.implementation.rest;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import me.lucko.luckperms.common.actionlog.LogPage;
|
||||
import me.lucko.luckperms.common.actionlog.LoggedAction;
|
||||
import me.lucko.luckperms.common.actionlog.filter.ActionFields;
|
||||
import me.lucko.luckperms.common.bulkupdate.BulkUpdate;
|
||||
import me.lucko.luckperms.common.context.ImmutableContextSetImpl;
|
||||
import me.lucko.luckperms.common.filter.Comparison;
|
||||
import me.lucko.luckperms.common.filter.Constraint;
|
||||
import me.lucko.luckperms.common.filter.Filter;
|
||||
import me.lucko.luckperms.common.filter.FilterField;
|
||||
import me.lucko.luckperms.common.filter.FilterList;
|
||||
import me.lucko.luckperms.common.filter.PageParameters;
|
||||
import me.lucko.luckperms.common.model.Group;
|
||||
import me.lucko.luckperms.common.model.Track;
|
||||
import me.lucko.luckperms.common.model.User;
|
||||
import me.lucko.luckperms.common.node.factory.NodeBuilders;
|
||||
import me.lucko.luckperms.common.node.matcher.ConstraintNodeMatcher;
|
||||
import me.lucko.luckperms.common.node.matcher.StandardNodeMatchers;
|
||||
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||
import me.lucko.luckperms.common.storage.StorageMetadata;
|
||||
import me.lucko.luckperms.common.storage.implementation.StorageImplementation;
|
||||
import me.lucko.luckperms.common.storage.misc.NodeEntry;
|
||||
import me.lucko.luckperms.common.storage.misc.PlayerSaveResultImpl;
|
||||
import me.lucko.luckperms.common.util.Difference;
|
||||
import me.lucko.luckperms.common.util.Iterators;
|
||||
import net.luckperms.api.actionlog.Action;
|
||||
import net.luckperms.api.context.ContextSet;
|
||||
import net.luckperms.api.model.PlayerSaveResult;
|
||||
import net.luckperms.api.node.Node;
|
||||
import net.luckperms.api.node.NodeBuilder;
|
||||
import net.luckperms.api.node.NodeType;
|
||||
import net.luckperms.rest.LuckPermsRestClient;
|
||||
import net.luckperms.rest.model.ActionPage;
|
||||
import net.luckperms.rest.model.Context;
|
||||
import net.luckperms.rest.model.CreateGroupRequest;
|
||||
import net.luckperms.rest.model.CreateTrackRequest;
|
||||
import net.luckperms.rest.model.CreateUserRequest;
|
||||
import net.luckperms.rest.model.GroupSearchResult;
|
||||
import net.luckperms.rest.model.Health;
|
||||
import net.luckperms.rest.model.UpdateTrackRequest;
|
||||
import net.luckperms.rest.model.UpdateUserRequest;
|
||||
import net.luckperms.rest.model.UserLookupResult;
|
||||
import net.luckperms.rest.model.UserSearchResult;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import retrofit2.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public class RestStorage implements StorageImplementation {
|
||||
private final LuckPermsPlugin plugin;
|
||||
private final LuckPermsRestClient client;
|
||||
|
||||
public RestStorage(LuckPermsPlugin plugin, String baseUrl, String apiKey) {
|
||||
this.plugin = plugin;
|
||||
this.client = LuckPermsRestClient.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.apiKey(apiKey)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LuckPermsPlugin getPlugin() {
|
||||
return this.plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getImplementationName() {
|
||||
return "REST";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() throws IOException {
|
||||
Health health = this.client.misc().health().execute().body();
|
||||
if (health == null || !health.healthy()) {
|
||||
this.plugin.getLogger().warn("REST storage endpoint is unhealthy");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
this.client.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageMetadata getMeta() {
|
||||
StorageMetadata metadata = new StorageMetadata();
|
||||
|
||||
boolean success = true;
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
Health health = this.client.misc().health().execute().body();
|
||||
if (health == null || !health.healthy()) {
|
||||
success = false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
int duration = (int) (System.currentTimeMillis() - start);
|
||||
metadata.ping(duration);
|
||||
}
|
||||
|
||||
metadata.connected(success);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logAction(Action entry) throws IOException {
|
||||
this.client.actions().submit(convertAction(entry)).execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogPage getLogPage(FilterList<Action> filters, @Nullable PageParameters page) throws Exception {
|
||||
Response<ActionPage> resp = null;
|
||||
|
||||
if (filters.isEmpty()) {
|
||||
// ActionFilters.all()
|
||||
resp = page != null
|
||||
? this.client.actions().query(page.pageSize(), page.pageNumber()).execute()
|
||||
: this.client.actions().query().execute();
|
||||
|
||||
} else if (filters.size() == 1) {
|
||||
// ActionFilters.source(uniqueId)
|
||||
Filter<Action, ?> filter = filters.get(0);
|
||||
if (filter.field() == ActionFields.SOURCE_UNIQUE_ID && filter.constraint().comparison() == Comparison.EQUAL) {
|
||||
UUID uniqueId = (UUID) filter.constraint().value();
|
||||
resp = page != null
|
||||
? this.client.actions().querySource(uniqueId, page.pageSize(), page.pageNumber()).execute()
|
||||
: this.client.actions().querySource(uniqueId).execute();
|
||||
}
|
||||
|
||||
} else if (filters.operator() == FilterList.LogicalOperator.AND && filters.size() == 2) {
|
||||
Filter<Action, ?> filterA = filters.get(0);
|
||||
Filter<Action, ?> filterB = filters.get(1);
|
||||
|
||||
if (filterA.field() == ActionFields.TARGET_TYPE && filterA.constraint().comparison() == Comparison.EQUAL) {
|
||||
Action.Target.Type type = (Action.Target.Type) filterA.constraint().value();
|
||||
|
||||
if (type == Action.Target.Type.USER) {
|
||||
// ActionFilters.user(uniqueId)
|
||||
if (filterB.field() == ActionFields.TARGET_UNIQUE_ID && filterB.constraint().comparison() == Comparison.EQUAL) {
|
||||
UUID uniqueId = (UUID) filterB.constraint().value();
|
||||
resp = page != null
|
||||
? this.client.actions().queryTargetUser(uniqueId, page.pageSize(), page.pageNumber()).execute()
|
||||
: this.client.actions().queryTargetUser(uniqueId).execute();
|
||||
}
|
||||
} else if (type == Action.Target.Type.GROUP) {
|
||||
// ActionFilters.group(name)
|
||||
if (filterB.field() == ActionFields.TARGET_NAME && filterB.constraint().comparison() == Comparison.EQUAL) {
|
||||
String name = (String) filterB.constraint().value();
|
||||
resp = page != null
|
||||
? this.client.actions().queryTargetGroup(name, page.pageSize(), page.pageNumber()).execute()
|
||||
: this.client.actions().queryTargetGroup(name).execute();
|
||||
}
|
||||
} else if (type == Action.Target.Type.TRACK) {
|
||||
// ActionFilters.track(name)
|
||||
if (filterB.field() == ActionFields.TARGET_NAME && filterB.constraint().comparison() == Comparison.EQUAL) {
|
||||
String name = (String) filterB.constraint().value();
|
||||
resp = page != null
|
||||
? this.client.actions().queryTargetTrack(name, page.pageSize(), page.pageNumber()).execute()
|
||||
: this.client.actions().queryTargetTrack(name).execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (filters.operator() == FilterList.LogicalOperator.OR && filters.size() == 3) {
|
||||
// ActionFilters.search(query)
|
||||
ImmutableList<FilterField<Action, String>> searchFields = ImmutableList.of(ActionFields.SOURCE_NAME, ActionFields.TARGET_NAME, ActionFields.DESCRIPTION);
|
||||
if (filters.stream().allMatch(filter -> filter.constraint().comparison() == Comparison.SIMILAR && searchFields.contains(filter.field()))) {
|
||||
String query = (String) filters.get(0).constraint().value();
|
||||
if (!query.startsWith("%") || !query.endsWith("%")) {
|
||||
throw new IllegalArgumentException("Unsupported search query: " + query);
|
||||
}
|
||||
String searchTerm = query.substring(1, query.length() - 1);
|
||||
|
||||
resp = page != null
|
||||
? this.client.actions().querySearch(searchTerm, page.pageSize(), page.pageNumber()).execute()
|
||||
: this.client.actions().querySearch(searchTerm).execute();
|
||||
}
|
||||
}
|
||||
|
||||
if (resp == null) {
|
||||
throw new UnsupportedOperationException("Unsupported filter: " + filters);
|
||||
}
|
||||
|
||||
ActionPage body = Objects.requireNonNull(resp.body(), "resp.body()");
|
||||
return LogPage.of(
|
||||
body.entries().stream().map(RestStorage::convertAction).collect(Collectors.toList()),
|
||||
page,
|
||||
body.overallSize()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyBulkUpdate(BulkUpdate bulkUpdate) {
|
||||
throw new UnsupportedOperationException(); // TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public User loadUser(UUID uniqueId, String username) throws Exception {
|
||||
net.luckperms.rest.model.User remoteUser = this.client.users().get(uniqueId).execute().body();
|
||||
if (remoteUser == null) {
|
||||
throw new IllegalStateException("Client did not return a user for " + uniqueId);
|
||||
}
|
||||
|
||||
User user = this.plugin.getUserManager().getOrMake(uniqueId, username);
|
||||
user.setUsername(remoteUser.username(), true);
|
||||
user.loadNodesFromStorage(remoteUser.nodes().stream().map(RestStorage::convertNode).collect(Collectors.toList()));
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<UUID, User> loadUsers(Set<UUID> uniqueIds) throws Exception {
|
||||
return uniqueIds.parallelStream()
|
||||
.map(uniqueId -> {
|
||||
try {
|
||||
return loadUser(uniqueId, null);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toMap(User::getUniqueId, Function.identity()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveUser(User user) throws Exception {
|
||||
Difference<Node> changes = user.normalData().exportChanges(results -> {
|
||||
if (this.plugin.getUserManager().isNonDefaultUser(user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if the only change is adding the default node, we don't need to export
|
||||
if (results.getChanges().size() == 1) {
|
||||
Difference.Change<Node> onlyChange = results.getChanges().iterator().next();
|
||||
return !(onlyChange.type() == Difference.ChangeType.ADD && this.plugin.getUserManager().isDefaultNode(onlyChange.value()));
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
if (changes == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String username = user.getUsername().orElse(null);
|
||||
if (username != null) {
|
||||
this.client.users().update(user.getUniqueId(), new UpdateUserRequest(username)).execute();
|
||||
}
|
||||
|
||||
Set<Node> added = changes.getAdded();
|
||||
Set<Node> removed = changes.getRemoved();
|
||||
|
||||
if (!removed.isEmpty()) {
|
||||
this.client.users().nodesDelete(user.getUniqueId(), removed.stream().map(RestStorage::convertNode).collect(Collectors.toList())).execute();
|
||||
}
|
||||
if (!added.isEmpty()) {
|
||||
this.client.users().nodesAdd(user.getUniqueId(), added.stream().map(RestStorage::convertNode).collect(Collectors.toList())).execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<UUID> getUniqueUsers() throws Exception {
|
||||
return this.client.users().list().execute().body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <N extends Node> List<NodeEntry<UUID, N>> searchUserNodes(ConstraintNodeMatcher<N> matcher) throws Exception {
|
||||
List<UserSearchResult> results;
|
||||
if (matcher instanceof StandardNodeMatchers.TypeEquals) {
|
||||
NodeType<? extends N> type = ((StandardNodeMatchers.TypeEquals<N>) matcher).getType();
|
||||
results = this.client.users().searchNodesByType(convertNodeType(type)).execute().body();
|
||||
} else {
|
||||
Constraint<String> constraint = matcher.getConstraint();
|
||||
Comparison comparison = constraint.comparison();
|
||||
String value = constraint.value();
|
||||
|
||||
if (comparison == Comparison.EQUAL) {
|
||||
results = this.client.users().searchNodesByKey(value).execute().body();
|
||||
} else if (comparison == Comparison.SIMILAR) {
|
||||
long wildcards = value.chars().filter(ch -> ch == '%').count();
|
||||
if (wildcards == 0) {
|
||||
results = this.client.users().searchNodesByKey(value).execute().body();
|
||||
} else if (wildcards == 1 && value.endsWith("%")) {
|
||||
results = this.client.users().searchNodesByKeyStartsWith(value.substring(0, value.length() - 1)).execute().body();
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unsupported constraint: " + constraint);
|
||||
}
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unsupported constraint: " + constraint);
|
||||
}
|
||||
}
|
||||
|
||||
if (results == null) {
|
||||
throw new IllegalStateException("Client returned null results");
|
||||
}
|
||||
|
||||
List<NodeEntry<UUID, N>> held = new ArrayList<>();
|
||||
for (UserSearchResult result : results) {
|
||||
UUID uniqueId = result.uniqueId();
|
||||
for (net.luckperms.rest.model.Node node : result.results()) {
|
||||
N match = matcher.filterConstraintMatch(convertNode(node));
|
||||
if (match != null) {
|
||||
held.add(NodeEntry.of(uniqueId, match));
|
||||
}
|
||||
}
|
||||
}
|
||||
return held;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Group createAndLoadGroup(String name) throws Exception {
|
||||
net.luckperms.rest.model.Group remoteGroup = this.client.groups().create(new CreateGroupRequest(name)).execute().body();
|
||||
if (remoteGroup == null) {
|
||||
remoteGroup = this.client.groups().get(name).execute().body();
|
||||
if (remoteGroup == null) {
|
||||
throw new IllegalStateException("Unable to create group: " + name);
|
||||
}
|
||||
}
|
||||
|
||||
Group group = this.plugin.getGroupManager().getOrMake(name);
|
||||
group.loadNodesFromStorage(remoteGroup.nodes().stream().map(RestStorage::convertNode).collect(Collectors.toList()));
|
||||
return group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Group> loadGroup(String name) throws Exception {
|
||||
net.luckperms.rest.model.Group remoteGroup = this.client.groups().get(name).execute().body();
|
||||
if (remoteGroup == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Group group = this.plugin.getGroupManager().getOrMake(name);
|
||||
group.loadNodesFromStorage(remoteGroup.nodes().stream().map(RestStorage::convertNode).collect(Collectors.toList()));
|
||||
return Optional.of(group);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadAllGroups() throws Exception {
|
||||
Set<String> groups = this.client.groups().list().execute().body();
|
||||
if (groups == null) {
|
||||
throw new IllegalStateException("Client returned a null list of groups");
|
||||
}
|
||||
|
||||
if (!Iterators.tryIterate(groups, this::loadGroup)) {
|
||||
throw new RuntimeException("Exception occurred whilst loading a group");
|
||||
}
|
||||
|
||||
this.plugin.getGroupManager().retainAll(groups);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveGroup(Group group) throws Exception {
|
||||
Difference<Node> changes = group.normalData().exportChanges(c -> true);
|
||||
|
||||
Set<Node> added = changes.getAdded();
|
||||
Set<Node> removed = changes.getRemoved();
|
||||
|
||||
if (!removed.isEmpty()) {
|
||||
this.client.groups().nodesDelete(group.getName(), removed.stream().map(RestStorage::convertNode).collect(Collectors.toList())).execute();
|
||||
}
|
||||
if (!added.isEmpty()) {
|
||||
this.client.groups().nodesAdd(group.getName(), added.stream().map(RestStorage::convertNode).collect(Collectors.toList())).execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteGroup(Group group) throws Exception {
|
||||
this.client.groups().delete(group.getName()).execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <N extends Node> List<NodeEntry<String, N>> searchGroupNodes(ConstraintNodeMatcher<N> matcher) throws Exception {
|
||||
List<GroupSearchResult> results;
|
||||
if (matcher instanceof StandardNodeMatchers.TypeEquals) {
|
||||
NodeType<? extends N> type = ((StandardNodeMatchers.TypeEquals<N>) matcher).getType();
|
||||
results = this.client.groups().searchNodesByType(convertNodeType(type)).execute().body();
|
||||
} else {
|
||||
Constraint<String> constraint = matcher.getConstraint();
|
||||
Comparison comparison = constraint.comparison();
|
||||
String value = constraint.value();
|
||||
|
||||
if (comparison == Comparison.EQUAL) {
|
||||
results = this.client.groups().searchNodesByKey(value).execute().body();
|
||||
} else if (comparison == Comparison.SIMILAR) {
|
||||
long wildcards = value.chars().filter(ch -> ch == '%').count();
|
||||
if (wildcards == 0) {
|
||||
results = this.client.groups().searchNodesByKey(value).execute().body();
|
||||
} else if (wildcards == 1 && value.endsWith("%")) {
|
||||
results = this.client.groups().searchNodesByKeyStartsWith(value.substring(0, value.length() - 1)).execute().body();
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unsupported constraint: " + constraint);
|
||||
}
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unsupported constraint: " + constraint);
|
||||
}
|
||||
}
|
||||
|
||||
if (results == null) {
|
||||
throw new IllegalStateException("Client returned null results");
|
||||
}
|
||||
|
||||
List<NodeEntry<String, N>> held = new ArrayList<>();
|
||||
for (GroupSearchResult result : results) {
|
||||
String name = result.name();
|
||||
for (net.luckperms.rest.model.Node node : result.results()) {
|
||||
N match = matcher.filterConstraintMatch(convertNode(node));
|
||||
if (match != null) {
|
||||
held.add(NodeEntry.of(name, match));
|
||||
}
|
||||
}
|
||||
}
|
||||
return held;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Track createAndLoadTrack(String name) throws Exception {
|
||||
net.luckperms.rest.model.Track remoteTrack = this.client.tracks().create(new CreateTrackRequest(name)).execute().body();
|
||||
if (remoteTrack == null) {
|
||||
remoteTrack = this.client.tracks().get(name).execute().body();
|
||||
if (remoteTrack == null) {
|
||||
throw new IllegalStateException("Unable to create track: " + name);
|
||||
}
|
||||
}
|
||||
|
||||
Track track = this.plugin.getTrackManager().getOrMake(name);
|
||||
track.setGroups(remoteTrack.groups());
|
||||
return track;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Track> loadTrack(String name) throws Exception {
|
||||
net.luckperms.rest.model.Track remoteTrack = this.client.tracks().get(name).execute().body();
|
||||
if (remoteTrack == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Track track = this.plugin.getTrackManager().getOrMake(name);
|
||||
track.setGroups(remoteTrack.groups());
|
||||
return Optional.of(track);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadAllTracks() throws Exception {
|
||||
Set<String> tracks = this.client.tracks().list().execute().body();
|
||||
if (tracks == null) {
|
||||
throw new IllegalStateException("Client returned a null list of tracks");
|
||||
}
|
||||
|
||||
if (!Iterators.tryIterate(tracks, this::loadTrack)) {
|
||||
throw new RuntimeException("Exception occurred whilst loading a track");
|
||||
}
|
||||
|
||||
this.plugin.getTrackManager().retainAll(tracks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveTrack(Track track) throws Exception {
|
||||
this.client.tracks().update(track.getName(), new UpdateTrackRequest(track.getGroups())).execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteTrack(Track track) throws Exception {
|
||||
this.client.tracks().delete(track.getName()).execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayerSaveResult savePlayerData(UUID uniqueId, String username) throws Exception {
|
||||
net.luckperms.rest.model.PlayerSaveResult remoteResult = this.client.users().create(new CreateUserRequest(uniqueId, username)).execute().body();
|
||||
|
||||
Set<PlayerSaveResult.Outcome> outcomes = remoteResult.outcomes().stream().map(outcome -> {
|
||||
switch (outcome) {
|
||||
case CLEAN_INSERT:
|
||||
return PlayerSaveResult.Outcome.CLEAN_INSERT;
|
||||
case NO_CHANGE:
|
||||
return PlayerSaveResult.Outcome.NO_CHANGE;
|
||||
case USERNAME_UPDATED:
|
||||
return PlayerSaveResult.Outcome.USERNAME_UPDATED;
|
||||
case OTHER_UNIQUE_IDS_PRESENT_FOR_USERNAME:
|
||||
return PlayerSaveResult.Outcome.OTHER_UNIQUE_IDS_PRESENT_FOR_USERNAME;
|
||||
default:
|
||||
throw new AssertionError(outcome);
|
||||
}
|
||||
}).collect(Collectors.toSet());
|
||||
|
||||
if (outcomes.isEmpty()) {
|
||||
throw new IllegalStateException("No outcomes returned");
|
||||
}
|
||||
|
||||
PlayerSaveResultImpl result;
|
||||
if (outcomes.contains(PlayerSaveResult.Outcome.CLEAN_INSERT)) {
|
||||
result = PlayerSaveResultImpl.cleanInsert();
|
||||
} else if (outcomes.contains(PlayerSaveResult.Outcome.NO_CHANGE)) {
|
||||
result = PlayerSaveResultImpl.noChange();
|
||||
} else if (outcomes.contains(PlayerSaveResult.Outcome.USERNAME_UPDATED)) {
|
||||
result = PlayerSaveResultImpl.usernameUpdated(remoteResult.previousUsername());
|
||||
} else {
|
||||
throw new IllegalStateException("No base outcome returned");
|
||||
}
|
||||
|
||||
if (outcomes.contains(PlayerSaveResult.Outcome.OTHER_UNIQUE_IDS_PRESENT_FOR_USERNAME)) {
|
||||
result = result.withOtherUuidsPresent(remoteResult.otherUniqueIds());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deletePlayerData(UUID uniqueId) throws Exception {
|
||||
this.client.users().delete(uniqueId, true).execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable UUID getPlayerUniqueId(String username) throws Exception {
|
||||
UserLookupResult result = this.client.users().lookup(username).execute().body();
|
||||
return result == null ? null : result.uniqueId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getPlayerName(UUID uniqueId) throws Exception {
|
||||
UserLookupResult result = this.client.users().lookup(uniqueId).execute().body();
|
||||
return result == null ? null : result.username();
|
||||
}
|
||||
|
||||
private static net.luckperms.rest.model.Action convertAction(Action action) {
|
||||
return new net.luckperms.rest.model.Action(
|
||||
action.getTimestamp().getEpochSecond(),
|
||||
new net.luckperms.rest.model.Action.Source(
|
||||
action.getSource().getUniqueId(),
|
||||
action.getSource().getName()
|
||||
),
|
||||
new net.luckperms.rest.model.Action.Target(
|
||||
action.getTarget().getUniqueId().orElse(null),
|
||||
action.getTarget().getName(),
|
||||
convertActionTargetType(action.getTarget().getType())
|
||||
),
|
||||
action.getDescription()
|
||||
);
|
||||
}
|
||||
|
||||
private static net.luckperms.rest.model.Action.Target.Type convertActionTargetType(Action.Target.Type type) {
|
||||
switch (type) {
|
||||
case USER:
|
||||
return net.luckperms.rest.model.Action.Target.Type.USER;
|
||||
case GROUP:
|
||||
return net.luckperms.rest.model.Action.Target.Type.GROUP;
|
||||
case TRACK:
|
||||
return net.luckperms.rest.model.Action.Target.Type.TRACK;
|
||||
default:
|
||||
throw new AssertionError(type);
|
||||
}
|
||||
}
|
||||
|
||||
private static LoggedAction convertAction(net.luckperms.rest.model.Action action) {
|
||||
return LoggedAction.build()
|
||||
.timestamp(Instant.ofEpochSecond(action.timestamp()))
|
||||
.source(action.source().uniqueId())
|
||||
.sourceName(action.source().name())
|
||||
.target(action.target().uniqueId())
|
||||
.targetName(action.target().name())
|
||||
.targetType(convertActionTargetType(action.target().type()))
|
||||
.description(action.description())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static Action.Target.Type convertActionTargetType(net.luckperms.rest.model.Action.Target.Type type) {
|
||||
switch (type) {
|
||||
case USER:
|
||||
return Action.Target.Type.USER;
|
||||
case GROUP:
|
||||
return Action.Target.Type.GROUP;
|
||||
case TRACK:
|
||||
return Action.Target.Type.TRACK;
|
||||
default:
|
||||
throw new AssertionError(type);
|
||||
}
|
||||
}
|
||||
|
||||
private static Node convertNode(net.luckperms.rest.model.Node node) {
|
||||
NodeBuilder<?, ?> builder = NodeBuilders.determineMostApplicable(node.key());
|
||||
if (node.value() != null) {
|
||||
builder.value(node.value());
|
||||
}
|
||||
if (node.context() != null) {
|
||||
builder.context(convertContexts(node.context()));
|
||||
}
|
||||
if (node.expiry() != null) {
|
||||
builder.expiry(node.expiry());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static net.luckperms.rest.model.Node convertNode(Node node) {
|
||||
return new net.luckperms.rest.model.Node(
|
||||
node.getKey(),
|
||||
node.getValue(),
|
||||
convertContexts(node.getContexts()),
|
||||
node.getExpiry() == null ? null : node.getExpiry().getEpochSecond()
|
||||
);
|
||||
}
|
||||
|
||||
public static ContextSet convertContexts(Set<Context> contexts) {
|
||||
ImmutableContextSetImpl.BuilderImpl builder = new ImmutableContextSetImpl.BuilderImpl();
|
||||
for (Context context : contexts) {
|
||||
builder.add(context.key(), context.value());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static Set<Context> convertContexts(ContextSet contexts) {
|
||||
return StreamSupport.stream(contexts.spliterator(), false)
|
||||
.map(c -> new Context(c.getKey(), c.getValue()))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public static net.luckperms.rest.model.NodeType convertNodeType(NodeType<?> type) {
|
||||
if (type == NodeType.REGEX_PERMISSION) return net.luckperms.rest.model.NodeType.REGEX_PERMISSION;
|
||||
if (type == NodeType.INHERITANCE) return net.luckperms.rest.model.NodeType.INHERITANCE;
|
||||
if (type == NodeType.PREFIX) return net.luckperms.rest.model.NodeType.PREFIX;
|
||||
if (type == NodeType.SUFFIX) return net.luckperms.rest.model.NodeType.SUFFIX;
|
||||
if (type == NodeType.META) return net.luckperms.rest.model.NodeType.META;
|
||||
if (type == NodeType.WEIGHT) return net.luckperms.rest.model.NodeType.WEIGHT;
|
||||
if (type == NodeType.DISPLAY_NAME) return net.luckperms.rest.model.NodeType.DISPLAY_NAME;
|
||||
throw new IllegalArgumentException("Invalid type: " + type.name());
|
||||
}
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* This file is part of LuckPerms, licensed under the MIT License.
|
||||
*
|
||||
* Copyright (c) lucko (Luck) <luck@lucko.me>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package me.lucko.luckperms.common.storage;
|
||||
|
||||
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||
import me.lucko.luckperms.common.storage.implementation.StorageImplementation;
|
||||
import me.lucko.luckperms.common.storage.implementation.rest.RestStorage;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.output.Slf4jLogConsumer;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
|
||||
@Tag("docker")
|
||||
public class RestStorageTest extends AbstractStorageTest {
|
||||
|
||||
private final GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("ghcr.io/luckperms/rest-api"))
|
||||
.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(RestStorageTest.class)))
|
||||
.withExposedPorts(8080)
|
||||
.waitingFor(new WaitAllStrategy()
|
||||
.withStrategy(Wait.forListeningPort())
|
||||
.withStrategy(Wait.forLogMessage(".*Successfully enabled.*", 1))
|
||||
);
|
||||
|
||||
@Override
|
||||
protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception {
|
||||
this.container.start();
|
||||
String host = this.container.getHost();
|
||||
Integer port = this.container.getFirstMappedPort();
|
||||
|
||||
return new RestStorage(plugin, "http://" + host + ":" + port + "/", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanupResources() {
|
||||
this.container.stop();
|
||||
}
|
||||
}
|
@@ -48,6 +48,7 @@ dependencies {
|
||||
testImplementation 'me.lucko.configurate:configurate-toml:3.7'
|
||||
testImplementation 'org.spongepowered:configurate-hocon:3.7.2'
|
||||
testImplementation 'org.yaml:snakeyaml:1.28'
|
||||
testImplementation 'net.luckperms:rest-api-java-client:0.1-SNAPSHOT'
|
||||
|
||||
testImplementation project(':standalone:app')
|
||||
testImplementation project(':common:loader-utils')
|
||||
|
@@ -58,7 +58,11 @@ import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.output.Slf4jLogConsumer;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
@@ -364,6 +368,35 @@ public class StorageIntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@Tag("docker")
|
||||
class Rest {
|
||||
|
||||
@Container
|
||||
private final GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("ghcr.io/luckperms/rest-api"))
|
||||
.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(StorageIntegrationTest.class)))
|
||||
.withExposedPorts(8080)
|
||||
.waitingFor(new WaitAllStrategy()
|
||||
.withStrategy(Wait.forListeningPort())
|
||||
.withStrategy(Wait.forLogMessage(".*Successfully enabled.*", 1))
|
||||
);
|
||||
|
||||
@Test
|
||||
public void testRest(@TempDir Path tempDir) {
|
||||
assertTrue(this.container.isRunning());
|
||||
|
||||
String host = this.container.getHost();
|
||||
Integer port = this.container.getFirstMappedPort();
|
||||
|
||||
Map<String, String> config = ImmutableMap.<String, String>builder()
|
||||
.put("storage-method", "rest")
|
||||
.put("data.rest-url", "http://" + host + ":" + port + "/")
|
||||
.build();
|
||||
|
||||
TestPluginProvider.use(tempDir, config, StorageIntegrationTest::testStorage);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class FlatFileDatabase {
|
||||
|
||||
|
Reference in New Issue
Block a user