/* * This file is part of LuckPerms, licensed under the MIT License. * * Copyright (c) lucko (Luck) <[email protected]> * 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.model; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; import me.lucko.luckperms.common.cacheddata.HolderCachedDataManager; import me.lucko.luckperms.common.cacheddata.type.MetaAccumulator; import me.lucko.luckperms.common.inheritance.InheritanceComparator; import me.lucko.luckperms.common.inheritance.InheritanceGraph; import me.lucko.luckperms.common.node.comparator.NodeWithContextComparator; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.query.DataSelector; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; import net.luckperms.api.model.data.TemporaryNodeMergeStrategy; import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeEqualityPredicate; import net.luckperms.api.node.NodeType; import net.luckperms.api.node.types.InheritanceNode; import net.luckperms.api.query.Flag; import net.luckperms.api.query.QueryOptions; import net.luckperms.api.util.Tristate; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.OptionalInt; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; import java.util.stream.Stream; /** * Represents an object that can hold permissions, (a user or group) * * <p>Data is stored in {@link NodeMap}s. A holder has two of these, one for * enduring nodes and one for transient nodes.</p> * * <p>This class provides a number of methods to perform inheritance lookups. * These lookup methods initially use Lists of nodes populated with the * inheritance tree. Nodes at the start of this list have priority over nodes at * the end. Nodes higher up the tree appear at the end of these lists. In order * to remove duplicate elements, the lists are flattened. This is significantly * faster than trying to prevent duplicates throughout the process of accumulation, * and reduces the need for too much caching.</p> * * <p>Cached state is avoided in these instances to cut down on memory * footprint. The nodes are stored indexed to the contexts they apply in, so * doing context specific querying should be fast. Caching would be ineffective * here, due to the potentially vast amount of contexts being used by nodes, * and the potential for very large inheritance trees.</p> */ public abstract class PermissionHolder { /** * Reference to the main plugin instance * @see #getPlugin() */ private final LuckPermsPlugin plugin; /** * The holders identifier */ private @MonotonicNonNull PermissionHolderIdentifier identifier; /** * The holders persistent nodes. * * <p>These (unlike transient nodes) are saved to the storage backing.</p> * * @see #normalData() */ private final NodeMap normalNodes = new NodeMap(this); /** * The holders transient nodes. * * <p>These are nodes which are never stored or persisted to a file, and * only last until the end of the objects lifetime. (for a group, that's * when the server stops, and for a user, it's when they log out, or get * unloaded.)</p> * * @see #transientData() */ private final NodeMap transientNodes = new NodeMap(this); /** * Lock used by Storage implementations to prevent concurrent read/writes * @see #getIoLock() */ private final Lock ioLock = new ReentrantLock(); /** * Comparator used to ordering groups when calculating inheritance */ private final Comparator<? super PermissionHolder> inheritanceComparator = InheritanceComparator.getFor(this); /** * Creates a new instance * * @param plugin the plugin instance */ protected PermissionHolder(LuckPermsPlugin plugin) { this.plugin = plugin; } // getters public LuckPermsPlugin getPlugin() { return this.plugin; } public Lock getIoLock() { return this.ioLock; } public Comparator<? super PermissionHolder> getInheritanceComparator() { return this.inheritanceComparator; } public NodeMap getData(DataType type) { switch (type) { case NORMAL: return this.normalNodes; case TRANSIENT: return this.transientNodes; default: throw new AssertionError(); } } public NodeMap normalData() { return this.normalNodes; } public NodeMap transientData() { return this.transientNodes; } public PermissionHolderIdentifier getIdentifier() { if (this.identifier == null) { this.identifier = new PermissionHolderIdentifier(getType(), getObjectName()); } return this.identifier; } /** * Gets the unique name of this holder object. * * <p>Used as a base for identifying permission holding objects. Also acts * as a method for preventing circular inheritance issues.</p> * * @return the object name */ public abstract String getObjectName(); /** * Gets the formatted display name of this permission holder * (for use in commands, etc) * * @return the holders formatted display name */ public abstract String getFormattedDisplayName(); /** * Gets a display name for this permission holder, without any formatting. * * @return the display name */ public abstract String getPlainDisplayName(); /** * Gets the most appropriate query options available at the time for the holder. * * @return query options */ public abstract QueryOptions getQueryOptions(); /** * Gets the holders cached data * * @return the holders cached data */ public abstract HolderCachedDataManager<?> getCachedData(); /** * Returns the type of this PermissionHolder. * * @return this holders type */ public abstract HolderType getType(); protected void invalidateCache() { this.normalNodes.invalidate(); this.transientNodes.invalidate(); getCachedData().invalidate(); getPlugin().getEventDispatcher().dispatchDataRecalculate(this); } public void setNodes(DataType type, Iterable<? extends Node> set) { getData(type).setContent(set); invalidateCache(); } public void setNodes(DataType type, Stream<? extends Node> stream) { getData(type).setContent(stream); invalidateCache(); } public void setNodes(DataType type, Multimap<ImmutableContextSet, ? extends Node> multimap) { getData(type).setContent(multimap.values()); invalidateCache(); } public void mergeNodes(DataType type, Iterable<? extends Node> set) { getData(type).mergeContent(set); invalidateCache(); } private DataType[] queryOrder(QueryOptions queryOptions) { return DataSelector.select(queryOptions, getIdentifier()); } public List<Node> getOwnNodes(QueryOptions queryOptions) { List<Node> nodes = new ArrayList<>(); for (DataType dataType : queryOrder(queryOptions)) { getData(dataType).copyTo(nodes, queryOptions); } return nodes; } public SortedSet<Node> getOwnNodesSorted(QueryOptions queryOptions) { SortedSet<Node> nodes = new TreeSet<>(NodeWithContextComparator.reverse()); for (DataType dataType : queryOrder(queryOptions)) { getData(dataType).copyTo(nodes, queryOptions); } return nodes; } public List<InheritanceNode> getOwnInheritanceNodes(QueryOptions queryOptions) { List<InheritanceNode> nodes = new ArrayList<>(); for (DataType dataType : queryOrder(queryOptions)) { getData(dataType).copyInheritanceNodesTo(nodes, queryOptions); } return nodes; } public <T extends Node> List<T> getOwnNodes(NodeType<T> type, QueryOptions queryOptions) { List<T> nodes = new ArrayList<>(); for (DataType dataType : queryOrder(queryOptions)) { getData(dataType).copyTo(nodes, type, queryOptions); } return nodes; } public List<Node> resolveInheritedNodes(QueryOptions queryOptions) { if (!queryOptions.flag(Flag.RESOLVE_INHERITANCE)) { return getOwnNodes(queryOptions); } List<Node> nodes = new ArrayList<>(); InheritanceGraph graph = this.plugin.getInheritanceGraphFactory().getGraph(queryOptions); for (PermissionHolder holder : graph.traverse(this)) { for (DataType dataType : holder.queryOrder(queryOptions)) { holder.getData(dataType).copyTo(nodes, queryOptions); } } return nodes; } public SortedSet<Node> resolveInheritedNodesSorted(QueryOptions queryOptions) { if (!queryOptions.flag(Flag.RESOLVE_INHERITANCE)) { return getOwnNodesSorted(queryOptions); } SortedSet<Node> nodes = new TreeSet<>(NodeWithContextComparator.reverse()); InheritanceGraph graph = this.plugin.getInheritanceGraphFactory().getGraph(queryOptions); for (PermissionHolder holder : graph.traverse(this)) { for (DataType dataType : holder.queryOrder(queryOptions)) { holder.getData(dataType).copyTo(nodes, queryOptions); } } return nodes; } public <T extends Node> List<T> resolveInheritedNodes(NodeType<T> type, QueryOptions queryOptions) { if (!queryOptions.flag(Flag.RESOLVE_INHERITANCE)) { return getOwnNodes(type, queryOptions); } List<T> nodes = new ArrayList<>(); InheritanceGraph graph = this.plugin.getInheritanceGraphFactory().getGraph(queryOptions); for (PermissionHolder holder : graph.traverse(this)) { for (DataType dataType : holder.queryOrder(queryOptions)) { holder.getData(dataType).copyTo(nodes, type, queryOptions); } } return nodes; } @SuppressWarnings({"unchecked", "rawtypes"}) public List<Group> resolveInheritanceTree(QueryOptions queryOptions) { InheritanceGraph graph = this.plugin.getInheritanceGraphFactory().getGraph(queryOptions); List<PermissionHolder> inheritanceTree = new ArrayList<>(); if (queryOptions.flag(Flag.RESOLVE_INHERITANCE)) { Iterables.addAll(inheritanceTree, graph.traverse(this)); inheritanceTree.remove(this); } else { // if RESOLVE_INHERITANCE is not set, only go up by one level Iterables.addAll(inheritanceTree, graph.successors(this)); } // ensure our tree now only consists of groups for (PermissionHolder permissionHolder : inheritanceTree) { if (!(permissionHolder instanceof Group)) { throw new IllegalStateException("Non-group object in inheritance tree: " + permissionHolder); } } // cast List<PermissionHolder> to List<Group> // this feels a bit dirty but it works & avoids needless copying! return (List) inheritanceTree; } public Map<String, Boolean> exportPermissions(QueryOptions queryOptions, boolean convertToLowercase, boolean resolveShorthand) { List<Node> entries = resolveInheritedNodes(queryOptions); return processExportedPermissions(entries, convertToLowercase, resolveShorthand); } private static ImmutableMap<String, Boolean> processExportedPermissions(List<Node> entries, boolean convertToLowercase, boolean resolveShorthand) { Map<String, Boolean> map = new HashMap<>(entries.size()); for (Node node : entries) { if (convertToLowercase) { map.putIfAbsent(node.getKey().toLowerCase(), node.getValue()); } else { map.putIfAbsent(node.getKey(), node.getValue()); } } if (resolveShorthand) { for (Node node : entries) { Collection<String> shorthand = node.resolveShorthand(); for (String s : shorthand) { if (convertToLowercase) { map.putIfAbsent(s.toLowerCase(), node.getValue()); } else { map.putIfAbsent(s, node.getValue()); } } } } return ImmutableMap.copyOf(map); } public MetaAccumulator accumulateMeta(QueryOptions queryOptions) { return accumulateMeta(MetaAccumulator.makeFromConfig(this.plugin), queryOptions); } public MetaAccumulator accumulateMeta(MetaAccumulator accumulator, QueryOptions queryOptions) { InheritanceGraph graph = this.plugin.getInheritanceGraphFactory().getGraph(queryOptions); for (PermissionHolder holder : graph.traverse(this)) { // accumulate nodes for (DataType dataType : holder.queryOrder(queryOptions)) { holder.getData(dataType).forEach(queryOptions, node -> { if (node.getValue() && NodeType.META_OR_CHAT_META.matches(node)) { accumulator.accumulateNode(node); } }); } // accumulate weight OptionalInt w = holder.getWeight(); if (w.isPresent()) { accumulator.accumulateWeight(w.getAsInt()); } } // accumulate primary group if (this instanceof User) { String primaryGroup = ((User) this).getPrimaryGroup().calculateValue(queryOptions); accumulator.setPrimaryGroup(primaryGroup); } accumulator.complete(); return accumulator; } /** * Removes temporary permissions that have expired * * @return true if permissions had expired and were removed */ public boolean auditTemporaryNodes() { boolean transientWork = auditTemporaryNodes(DataType.TRANSIENT); boolean normalWork = auditTemporaryNodes(DataType.NORMAL); return transientWork || normalWork; } private boolean auditTemporaryNodes(DataType dataType) { ImmutableCollection<? extends Node> before = getData(dataType).immutable().values(); Set<Node> removed = new HashSet<>(); boolean work = getData(dataType).auditTemporaryNodes(removed); if (work) { // invalidate invalidateCache(); // call event ImmutableCollection<? extends Node> after = getData(dataType).immutable().values(); for (Node r : removed) { this.plugin.getEventDispatcher().dispatchNodeRemove(r, this, dataType, before, after); } } return work; } public Tristate hasNode(DataType type, Node node, NodeEqualityPredicate equalityPredicate) { if (this.getType() == HolderType.GROUP && node instanceof InheritanceNode && ((InheritanceNode) node).getGroupName().equalsIgnoreCase(getObjectName())) { return Tristate.TRUE; } return getData(type).immutable().values().stream() .filter(equalityPredicate.equalTo(node)) .findFirst() .map(n -> Tristate.of(n.getValue())).orElse(Tristate.UNDEFINED); } public DataMutateResult setNode(DataType dataType, Node node, boolean callEvent) { if (hasNode(dataType, node, NodeEqualityPredicate.IGNORE_EXPIRY_TIME) != Tristate.UNDEFINED) { return DataMutateResult.FAIL_ALREADY_HAS; } NodeMap data = getData(dataType); ImmutableCollection<? extends Node> before = data.immutable().values(); data.add(node); invalidateCache(); ImmutableCollection<? extends Node> after = data.immutable().values(); if (callEvent) { this.plugin.getEventDispatcher().dispatchNodeAdd(node, this, dataType, before, after); } return DataMutateResult.SUCCESS; } public DataMutateResult.WithMergedNode setNode(DataType dataType, Node node, TemporaryNodeMergeStrategy mergeStrategy) { if (node.getExpiry() != null && mergeStrategy != TemporaryNodeMergeStrategy.NONE) { Node otherMatch = getData(dataType).immutable().values().stream() .filter(NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE.equalTo(node)) .findFirst().orElse(null); if (otherMatch != null && otherMatch.getExpiry() != null) { NodeMap data = getData(dataType); Node newNode = null; switch (mergeStrategy) { case ADD_NEW_DURATION_TO_EXISTING: { // Create a new Node with the same properties, but add the expiry dates together Instant newExpiry = otherMatch.getExpiry().plus(Duration.between(Instant.now(), node.getExpiry())); newNode = node.toBuilder().expiry(newExpiry).build(); break; } case REPLACE_EXISTING_IF_DURATION_LONGER: { // Only replace if the new expiry time is greater than the old one. if (node.getExpiry().compareTo(otherMatch.getExpiry()) > 0) { newNode = node; } break; } } if (newNode != null) { // Remove the old Node & add the new one. ImmutableCollection<? extends Node> before = data.immutable().values(); data.replace(newNode, otherMatch); invalidateCache(); ImmutableCollection<? extends Node> after = data.immutable().values(); this.plugin.getEventDispatcher().dispatchNodeAdd(newNode, this, dataType, before, after); return new MergedNodeResult(DataMutateResult.SUCCESS, newNode); } } } // Fallback to the normal handling. return new MergedNodeResult(setNode(dataType, node, true), node); } public DataMutateResult unsetNode(DataType dataType, Node node) { if (hasNode(dataType, node, NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE) == Tristate.UNDEFINED) { return DataMutateResult.FAIL_LACKS; } ImmutableCollection<? extends Node> before = getData(dataType).immutable().values(); getData(dataType).remove(node); invalidateCache(); ImmutableCollection<? extends Node> after = getData(dataType).immutable().values(); this.plugin.getEventDispatcher().dispatchNodeRemove(node, this, dataType, before, after); return DataMutateResult.SUCCESS; } public DataMutateResult.WithMergedNode unsetNode(DataType dataType, Node node, @Nullable Duration duration) { if (node.getExpiry() != null && duration != null) { Node otherMatch = getData(dataType).immutable().values().stream() .filter(NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE.equalTo(node)) .findFirst().orElse(null); if (otherMatch != null && otherMatch.getExpiry() != null) { NodeMap data = getData(dataType); Instant newExpiry = otherMatch.getExpiry().minus(duration); if (newExpiry.isAfter(Instant.now())) { Node newNode = node.toBuilder().expiry(newExpiry).build(); // Remove the old Node & add the new one. ImmutableCollection<? extends Node> before = data.immutable().values(); data.replace(newNode, otherMatch); invalidateCache(); ImmutableCollection<? extends Node> after = data.immutable().values(); this.plugin.getEventDispatcher().dispatchNodeRemove(otherMatch, this, dataType, before, after); this.plugin.getEventDispatcher().dispatchNodeAdd(newNode, this, dataType, before, after); return new MergedNodeResult(DataMutateResult.SUCCESS, newNode); } } } // Fallback to the normal handling. return new MergedNodeResult(unsetNode(dataType, node), null); } public boolean removeIf(DataType dataType, @Nullable ContextSet contextSet, Predicate<? super Node> predicate, boolean giveDefault) { NodeMap data = getData(dataType); ImmutableCollection<? extends Node> before = data.immutable().values(); if (contextSet == null) { if (!data.removeIf(predicate)) { return false; } } else { if (!data.removeIf(contextSet, predicate)) { return false; } } if (getType() == HolderType.USER && giveDefault) { getPlugin().getUserManager().giveDefaultIfNeeded((User) this, false); } invalidateCache(); ImmutableCollection<? extends Node> after = data.immutable().values(); this.plugin.getEventDispatcher().dispatchNodeClear(this, dataType, before, after); return true; } public boolean clearNodes(DataType dataType, ContextSet contextSet, boolean giveDefault) { NodeMap data = getData(dataType); ImmutableCollection<? extends Node> before = data.immutable().values(); if (contextSet == null) { data.clear(); } else { data.clear(contextSet); } if (getType() == HolderType.USER && giveDefault) { getPlugin().getUserManager().giveDefaultIfNeeded((User) this, false); } invalidateCache(); ImmutableCollection<? extends Node> after = data.immutable().values(); if (before.size() == after.size()) { return false; } this.plugin.getEventDispatcher().dispatchNodeClear(this, dataType, before, after); return true; } public OptionalInt getWeight() { return OptionalInt.empty(); } private static final class MergedNodeResult implements DataMutateResult.WithMergedNode { private final DataMutateResult result; private final Node mergedNode; private MergedNodeResult(DataMutateResult result, Node mergedNode) { this.result = result; this.mergedNode = mergedNode; } @Override public @NonNull DataMutateResult getResult() { return this.result; } @Override public @NonNull Node getMergedNode() { return this.mergedNode; } } }