001/*
002 * PermissionsEx
003 * Copyright (C) zml and PermissionsEx contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package ca.stellardrift.permissionsex.impl;
018
019import ca.stellardrift.permissionsex.PermissionsEngine;
020import ca.stellardrift.permissionsex.context.ContextDefinition;
021import ca.stellardrift.permissionsex.context.ContextDefinitionProvider;
022import ca.stellardrift.permissionsex.context.ContextInheritance;
023import ca.stellardrift.permissionsex.context.SimpleContextDefinition;
024import ca.stellardrift.permissionsex.datastore.ConversionResult;
025import ca.stellardrift.permissionsex.datastore.DataStore;
026import ca.stellardrift.permissionsex.datastore.DataStoreContext;
027import ca.stellardrift.permissionsex.datastore.DataStoreFactory;
028import ca.stellardrift.permissionsex.datastore.ProtoDataStore;
029import ca.stellardrift.permissionsex.impl.backend.memory.MemoryDataStore;
030import ca.stellardrift.permissionsex.impl.config.PermissionsExConfiguration;
031import ca.stellardrift.permissionsex.exception.PEBKACException;
032import ca.stellardrift.permissionsex.exception.PermissionsLoadingException;
033import ca.stellardrift.permissionsex.impl.context.PEXContextDefinition;
034import ca.stellardrift.permissionsex.impl.context.ServerTagContextDefinition;
035import ca.stellardrift.permissionsex.impl.context.TimeContextDefinition;
036import ca.stellardrift.permissionsex.impl.util.CacheListenerHolder;
037import ca.stellardrift.permissionsex.impl.rank.RankLadderCache;
038import ca.stellardrift.permissionsex.impl.subject.SubjectDataCacheImpl;
039import ca.stellardrift.permissionsex.impl.subject.ToDataSubjectRefImpl;
040import ca.stellardrift.permissionsex.impl.logging.DebugPermissionCheckNotifier;
041import ca.stellardrift.permissionsex.impl.subject.LazySubjectRef;
042import ca.stellardrift.permissionsex.impl.util.PCollections;
043import ca.stellardrift.permissionsex.logging.PermissionCheckNotifier;
044import ca.stellardrift.permissionsex.impl.logging.RecordingPermissionCheckNotifier;
045import ca.stellardrift.permissionsex.logging.FormattedLogger;
046import ca.stellardrift.permissionsex.impl.logging.WrappingFormattedLogger;
047import ca.stellardrift.permissionsex.impl.subject.CalculatedSubjectImpl;
048import ca.stellardrift.permissionsex.rank.RankLadderCollection;
049import ca.stellardrift.permissionsex.subject.SubjectRef;
050import ca.stellardrift.permissionsex.subject.SubjectType;
051import ca.stellardrift.permissionsex.impl.subject.SubjectTypeCollectionImpl;
052import ca.stellardrift.permissionsex.impl.util.Util;
053import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
054import org.checkerframework.checker.nullness.qual.Nullable;
055import org.pcollections.PVector;
056import org.pcollections.TreePVector;
057
058import javax.sql.DataSource;
059import java.io.IOException;
060import java.nio.file.Path;
061import java.sql.SQLException;
062import java.util.Collection;
063import java.util.Collections;
064import java.util.HashSet;
065import java.util.List;
066import java.util.Map;
067import java.util.Set;
068import java.util.concurrent.CompletableFuture;
069import java.util.concurrent.ConcurrentHashMap;
070import java.util.concurrent.ConcurrentMap;
071import java.util.concurrent.Executor;
072import java.util.concurrent.atomic.AtomicReference;
073import java.util.function.Consumer;
074import java.util.function.Function;
075import java.util.function.Supplier;
076import java.util.regex.Pattern;
077import java.util.stream.Collectors;
078
079import static ca.stellardrift.permissionsex.impl.Messages.*;
080import static java.util.Objects.requireNonNull;
081
082
083/**
084 * The entry point to the PermissionsEx engine.
085 *
086 * <p>The fastest way to get going with working with subjects is to access a subject type collection
087 * with {@link #subjects(SubjectType)} and request a {@link CalculatedSubjectImpl} to query data
088 * from. Directly working with {@link ToDataSubjectRefImpl}s is another option, preferable if most
089 * of the operations being performed are writes, or querying data directly defined on a subject.</p>
090 *
091 * <p>Keep in mind most of PEX's core data objects are immutable and must be resubmitted to their
092 * holders to apply updates. Most write operations are done asynchronously, and futures are returned
093 * that complete when the backend is finished writing out data. For larger operations, it can be
094 * useful to perform changes within {@link #performBulkOperation(Supplier)}, which will reduce
095 * unnecessary writes to the backing data store in some cases.</p>
096 */
097public class PermissionsEx<P> implements Consumer<ContextInheritance>,
098        ContextDefinitionProvider,
099        PermissionsEngine,
100        DataStoreContext {
101
102    private final FormattedLogger logger;
103    private final ImplementationInterface impl;
104    private final MemoryDataStore transientData;
105    private final RecordingPermissionCheckNotifier baseNotifier = new RecordingPermissionCheckNotifier();
106    private volatile PermissionCheckNotifier notifier = baseNotifier;
107    private final ConcurrentMap<String, ContextDefinition<?>> contextTypes = new ConcurrentHashMap<>();
108
109    private final AtomicReference<@Nullable State<P>> state = new AtomicReference<>();
110    private final ConcurrentMap<String, SubjectTypeCollectionImpl<?>> subjectTypeCache = new ConcurrentHashMap<>();
111    private @MonotonicNonNull RankLadderCache rankLadderCache;
112    private volatile @Nullable CompletableFuture<ContextInheritance> cachedInheritance;
113    private final CacheListenerHolder<Boolean, ContextInheritance> cachedInheritanceListeners = new CacheListenerHolder<>();
114
115    private static class State<P> {
116        private final PermissionsExConfiguration<P> config;
117        private final DataStore activeDataStore;
118        private PVector<ConversionResult> availableConversions = TreePVector.empty();
119
120        private State(PermissionsExConfiguration<P> config, DataStore activeDataStore) {
121            this.config = config;
122            this.activeDataStore = activeDataStore;
123        }
124    }
125
126    public PermissionsEx(final PermissionsExConfiguration<P> config, ImplementationInterface impl) throws PermissionsLoadingException {
127        this.impl = impl;
128        this.logger = WrappingFormattedLogger.of(impl.logger(), false);
129        this.transientData = (MemoryDataStore) MemoryDataStore.create("transient").defrost(this);
130        this.debugMode(config.isDebugEnabled());
131        this.registerContextDefinitions(
132                ServerTagContextDefinition.INSTANCE,
133                TimeContextDefinition.BEFORE_TIME,
134                TimeContextDefinition.AFTER_TIME);
135        this.initialize(config);
136
137        this.subjects(SUBJECTS_DEFAULTS);
138        this.subjects(SUBJECTS_FALLBACK);
139    }
140
141    private State<P> getState() throws IllegalStateException {
142        final @Nullable State<P> ret = this.state.get();
143        if (ret == null) {
144            throw new IllegalStateException("Manager has already been closed!");
145        }
146        return ret;
147    }
148
149
150    /**
151     * Get the collection of subjects of a given type. No data is loaded in this operation.
152     * Any string is supported as a subject type, but some common types have been provided as constants
153     * in this class for convenience.
154     *
155     * @see PermissionsEngine#SUBJECTS_DEFAULTS
156     * @param type The type identifier requested. Can be any string
157     * @return The subject type collection
158     */
159    @Override
160    public <I> SubjectTypeCollectionImpl<I> subjects(final SubjectType<I> type) {
161        @SuppressWarnings("unchecked")
162        final SubjectTypeCollectionImpl<I> collection = (SubjectTypeCollectionImpl<I>) this.subjectTypeCache.computeIfAbsent(type.name(),
163                key -> new SubjectTypeCollectionImpl<>(
164                        this,
165                        type,
166                        new SubjectDataCacheImpl<>(type, getState().activeDataStore),
167                        new SubjectDataCacheImpl<>(type, transientData)));
168        if (!type.equals(collection.type())) {
169            throw new IllegalArgumentException("Provided subject type " + type + " is different from registered type " + collection.type());
170        }
171        return collection;
172    }
173
174    /**
175     * Get a view of the currently cached subject types
176     *
177     * @return Unmodifiable view of the currently cached subject types
178     */
179    @Override
180    public Collection<SubjectTypeCollectionImpl<?>> loadedSubjectTypes() {
181        return Collections.unmodifiableCollection(this.subjectTypeCache.values());
182    }
183
184    /**
185     * Get all registered subject types in the active data store.
186     * The set is an immutable copy of the backend data.
187     *
188     * @return A set of registered subject types
189     */
190    @Override
191    public Set<SubjectType<?>> knownSubjectTypes() {
192        return this.subjectTypeCache.values().stream().map(SubjectTypeCollectionImpl::type).collect(Collectors.toSet());
193    }
194
195    // -- DataStoreContext -- //
196
197    @Override
198    public PermissionsEngine engine() {
199        return this;
200    }
201
202    @Override
203    public SubjectRef<?> deserializeSubjectRef(final String type, final String name) {
204        final @Nullable SubjectTypeCollectionImpl<?> existingCollection = this.subjectTypeCache.get(type);
205        if (existingCollection == null) {
206            throw new IllegalArgumentException("Unknown subject type " + type);
207        }
208        return deserialize(existingCollection.type(), name);
209    }
210
211    @Override
212    public SubjectRef<?> lazySubjectRef(String type, String identifier) {
213        return new LazySubjectRef(
214                this,
215                requireNonNull(type, "type"),
216                requireNonNull(identifier, "identifier")
217        );
218    }
219
220    private <I> SubjectRef<I> deserialize(final SubjectType<I> type, final String serializedIdent) {
221        return SubjectRef.subject(type, type.parseIdentifier(serializedIdent));
222    }
223
224    @Override
225    public <V> CompletableFuture<V> doBulkOperation(Function<DataStore, CompletableFuture<V>> actor) {
226        return this.getState().activeDataStore.performBulkOperation(actor).thenCompose(it -> it);
227    }
228
229    /**
230     * Suppress writes to the data store for the duration of a specific operation. Only really useful for extremely large operations
231     *
232     * @param func The operation to perform
233     * @param <T> The type of data that will be returned
234     * @return A future that completes once all data has been written to the store
235     */
236    public <T> CompletableFuture<T> performBulkOperation(Supplier<CompletableFuture<T>> func) {
237        return getState().activeDataStore.performBulkOperation(store -> func.get().join());
238    }
239
240    /**
241     * Access rank ladders through a cached interface
242     *
243     * @return Access to rank ladders
244     */
245    @Override
246    public RankLadderCollection ladders() {
247        return this.rankLadderCache;
248    }
249
250    /**
251     * Imports data into the currently active backend from another configured backend.
252     *
253     * @param dataStoreIdentifier The identifier of the backend to import from
254     * @return A future that completes once the import operation is complete
255     */
256    public CompletableFuture<?> importDataFrom(String dataStoreIdentifier) {
257        final State<P> state = getState();
258        final @Nullable ProtoDataStore<?> expected = state.config.getDataStore(dataStoreIdentifier);
259        if (expected == null) {
260            return Util.failedFuture(new IllegalArgumentException("Data store " + dataStoreIdentifier + " is not present"));
261        }
262        return importDataFrom(expected);
263    }
264
265    public CompletableFuture<?> importDataFrom(ConversionResult conversion) {
266        return importDataFrom(conversion.store());
267    }
268
269    private CompletableFuture<?> importDataFrom(final ProtoDataStore<?> request) {
270        final State<P> state = getState();
271        final DataStore expected;
272        try {
273            expected = request.defrost(this);
274        } catch (PermissionsLoadingException e) {
275            return Util.failedFuture(e);
276        }
277
278        return state.activeDataStore.performBulkOperation(store -> {
279            CompletableFuture<?> result = CompletableFuture.allOf(expected.getAll().map(subject -> store.setData(subject.getKey(), subject.getValue())).toArray(CompletableFuture[]::new)); // subjects
280            result = result.thenCombine(expected.getContextInheritance(null).thenCompose(store::setContextInheritance), (a, b) -> a); // context inheritance
281            result = expected.getAllRankLadders()
282                    .map(ladder -> expected.getRankLadder(ladder, null).thenCompose(ladderData -> store.setRankLadder(ladder, ladderData))) // combine all rank ladder futures
283                    .reduce(result, (existing, next) -> existing.thenCombine(next, (v, a) -> null), (one, two) -> one.thenCombine(two, (v, a) -> null));
284            return result;
285        }).thenCompose(x -> x);
286    }
287
288    /**
289     * Get the currently active notifier. This object has callbacks triggered on every permission check
290     *
291     * @return The active notifier
292     */
293    public PermissionCheckNotifier getNotifier() {
294        return this.notifier;
295    }
296
297    /**
298     * Get the base notifier that logs any permission checks that gave taken place.
299     * @return the notifier, even if not active
300     */
301    public RecordingPermissionCheckNotifier getRecordingNotifier() {
302        return this.baseNotifier;
303    }
304
305    // TODO: Proper thread-safety
306
307    /**
308     * Know whether or not debug mode is enabled
309     *
310     * @return true if debug mode is enabled
311     */
312    @Override
313    public boolean debugMode() {
314        return this.getNotifier() instanceof DebugPermissionCheckNotifier;
315    }
316
317    /**
318     * Set whether or not debug mode is enabled. Debug mode logs all permission, option, and inheritance
319     * checks made to the console.
320     *
321     * @param debug Whether to enable debug mode
322     * @param filterPattern A pattern to filter which permissions are logged. Null for no filter.
323     */
324    @Override
325    public synchronized void debugMode(boolean debug, final @Nullable Pattern filterPattern) {
326        if (debug) {
327            if (this.notifier instanceof DebugPermissionCheckNotifier) {
328                this.notifier = new DebugPermissionCheckNotifier(this.logger(), ((DebugPermissionCheckNotifier) this.notifier).getDelegate(), filterPattern == null ? null : perm -> filterPattern.matcher(perm).find());
329            } else {
330                this.notifier = new DebugPermissionCheckNotifier(this.logger(), this.notifier, filterPattern == null ? null : perm -> filterPattern.matcher(perm).find());
331            }
332        } else {
333            if (this.notifier instanceof DebugPermissionCheckNotifier) {
334                this.notifier = ((DebugPermissionCheckNotifier) this.notifier).getDelegate();
335            }
336        }
337    }
338
339    /**
340     * Synchronous helper to perform reloads
341     *
342     * @throws PEBKACException If the configuration couldn't be parsed
343     * @throws PermissionsLoadingException When there's an error loading the data store
344     */
345    private void reloadSync() throws PEBKACException, PermissionsLoadingException {
346        try {
347            PermissionsExConfiguration<P> config = getState().config.reload();
348            config.validate();
349            initialize(config);
350            // TODO: Throw reload event to cache any relevant subject types
351        } catch (IOException e) {
352            throw new PEBKACException(CONFIG_ERROR_LOAD.tr(e.getLocalizedMessage()));
353        }
354    }
355
356    /**
357     * Initialize the engine.
358     *
359     * May be called even if the engine has been initialized already, with results essentially equivalent to performing a reload
360     *
361     * @param config The configuration to use in this engine
362     * @throws PermissionsLoadingException If an error occurs loading the backend
363     */
364    private void initialize(final PermissionsExConfiguration<P> config) throws PermissionsLoadingException {
365        final DataStore newStore = config.getDefaultDataStore().defrost(this);
366        State<P> newState = new State<>(config, newStore);
367        boolean shouldAnnounceImports = !newState.activeDataStore.firstRun();
368        try {
369            newState.config.save();
370        } catch (IOException e) {
371            throw new PermissionsLoadingException(CONFIG_ERROR_SAVE.tr(), e);
372        }
373
374        if (shouldAnnounceImports) {
375            this.logger().warn(CONVERSION_BANNER.tr());
376        }
377
378        PVector<ConversionResult> allResults = TreePVector.empty();
379        for (final DataStoreFactory<?> convertable : DataStoreFactory.all().values()) {
380            if (!(convertable instanceof DataStoreFactory.Convertable))  {
381                continue;
382            }
383            final DataStoreFactory.Convertable<?> prov = ((DataStoreFactory.Convertable<?>) convertable);
384
385            List<ConversionResult> res = prov.listConversionOptions(this);
386            if (!res.isEmpty()) {
387                if (shouldAnnounceImports) {
388                    this.logger().info(CONVERSION_PLUGINHEADER.tr(prov.friendlyName()));
389                    for (ConversionResult result : res) {
390                        this.logger().info(CONVERSION_INSTANCE.tr(result.description(), result.store().identifier()));
391                    }
392                }
393                allResults = allResults.plusAll(res);
394            }
395        }
396        newState.availableConversions = allResults;
397
398        final @Nullable State<P> oldState = this.state.getAndSet(newState);
399        if (oldState != null) {
400            try {
401                oldState.activeDataStore.close();
402            } catch (final Exception ignore) {} // TODO maybe warn?
403        }
404
405        this.rankLadderCache = new RankLadderCache(this.rankLadderCache, newState.activeDataStore);
406        this.subjectTypeCache.forEach((key, val) -> val.update(newState.activeDataStore));
407        this.contextTypes.values().forEach(ctxDef -> {
408            if (ctxDef instanceof PEXContextDefinition<?>) {
409                ((PEXContextDefinition<?>) ctxDef).update(newState.config);
410            }
411        });
412        if (this.cachedInheritance != null) {
413            this.cachedInheritance = null;
414            contextInheritance((Consumer<ContextInheritance>) null).thenAccept(inheritance -> this.cachedInheritanceListeners.call(true, inheritance));
415        }
416
417        // Migrate over legacy subject data
418        newState.activeDataStore.moveData("system", SUBJECTS_DEFAULTS.name(), SUBJECTS_DEFAULTS.name(), SUBJECTS_DEFAULTS.name()).thenRun(() -> {
419            this.logger().info(CONVERSION_RESULT_SUCCESS.tr());
420        });
421    }
422
423    /**
424     * Reload the configuration file in use and refresh backend data
425     *
426     * @return A future that completes once a reload has finished
427     */
428    public CompletableFuture<Void> reload() {
429        return Util.asyncFailableFuture(() -> {
430            reloadSync();
431            return null;
432        }, this.asyncExecutor());
433    }
434
435    /**
436     * Shut down the PEX engine. Once this has been done, no further action can be taken
437     * until the engine is reinitialized with a fresh configuration.
438     */
439    public void close() {
440        final @Nullable State<P> state = this.state.getAndSet(null);
441        if (state != null) {
442            state.activeDataStore.close();
443        }
444    }
445
446    public List<ConversionResult> getAvailableConversions() {
447        return getState().availableConversions;
448    }
449
450    @Override
451    public FormattedLogger logger() {
452        return this.logger;
453    }
454
455    // -- Implementation interface proxy methods --
456
457    @Override
458    public Path baseDirectory() {
459        return impl.baseDirectory();
460    }
461
462    public Path baseDirectory(BaseDirectoryScope scope) {
463        return impl.baseDirectory(scope);
464    }
465
466    @Override
467    @Deprecated
468    public @Nullable DataSource dataSourceForUrl(String url) throws SQLException {
469        return impl.dataSourceForUrl(url);
470    }
471
472    /**
473     * Get an executor to run tasks asynchronously on.
474     *
475     * @return The async executor
476     */
477    @Override
478    public Executor asyncExecutor() {
479        return impl.asyncExecutor();
480    }
481
482    public String version() {
483        return impl.version();
484    }
485
486    /**
487     * Get the current configuration PEX is operating with. This object is immutable.
488     *
489     * @return The current configuration object
490     */
491    public PermissionsExConfiguration<P> config() {
492        return getState().config;
493    }
494
495    public DataStore activeDataStore() {
496        return getState().activeDataStore;
497    }
498
499    /**
500     * Get context inheritance data.
501     *
502     * <p>The result of the future is immutable -- to take effect, the object returned by any
503     * update methods in {@link ContextInheritance} must be passed to {@link #contextInheritance(ContextInheritance)}.
504     *  It follows that anybody else's changes will not appear in the returned inheritance object -- so if updates are
505     *  desired providing a callback function is important.</p>
506     *
507     * @param listener A callback function that will be triggered whenever there is a change to the context inheritance
508     * @return A future providing the current context inheritance data
509     */
510    @Override
511    public CompletableFuture<ContextInheritance> contextInheritance(final @Nullable Consumer<ContextInheritance> listener) {
512        if (this.cachedInheritance == null) {
513            this.cachedInheritance = getState().activeDataStore.getContextInheritance(this);
514        }
515        if (listener != null) {
516            this.cachedInheritanceListeners.addListener(true, listener);
517        }
518        return this.cachedInheritance;
519
520    }
521
522    /**
523     * Update the context inheritance when values have been changed
524     *
525     * @param newInheritance The modified inheritance object
526     * @return A future containing the latest context inheritance object
527     */
528    @Override
529    public CompletableFuture<ContextInheritance> contextInheritance(ContextInheritance newInheritance) {
530        return getState().activeDataStore.setContextInheritance(newInheritance);
531    }
532
533    /**
534     * Listener method that handles changes to context inheritance. Should not be called by outside users
535     *
536     * @param newData The new data to replace cached information
537     */
538    @Override
539    public void accept(ContextInheritance newData) {
540        this.cachedInheritance = CompletableFuture.completedFuture(newData);
541        this.cachedInheritanceListeners.call(true, newData);
542    }
543
544    @Override
545    public CompletableFuture<Set<ContextDefinition<?>>> usedContextTypes() {
546        return getState().activeDataStore.getDefinedContextKeys().thenCombine(transientData.getDefinedContextKeys(), (persist, trans) -> {
547            final Set<ContextDefinition<?>> build = new HashSet<>();
548            for (final ContextDefinition<?> def : this.contextTypes.values()) {
549                if (persist.contains(def.name()) || trans.contains(def.name())) {
550                    build.add(def);
551                }
552            }
553           return Collections.unmodifiableSet(build);
554        });
555    }
556
557    @Override
558    public <T> boolean registerContextDefinition(ContextDefinition<T> contextDefinition) {
559        if (contextDefinition instanceof PEXContextDefinition<?> && this.state.get() != null) {
560            ((PEXContextDefinition<T>) contextDefinition).update(config());
561        }
562       final @Nullable ContextDefinition<?> possibleOut =  this.contextTypes.putIfAbsent(contextDefinition.name(), contextDefinition);
563        if (possibleOut instanceof SimpleContextDefinition.Fallback) {
564            return this.contextTypes.replace(contextDefinition.name(), possibleOut, contextDefinition);
565        } else {
566            return possibleOut == null;
567        }
568    }
569
570    @Override
571    public int registerContextDefinitions(ContextDefinition<?>... definitions) {
572        int numRegistered = 0;
573        for (ContextDefinition<?> def : definitions) {
574            if (registerContextDefinition(def)) {
575                numRegistered++;
576            }
577        }
578        return numRegistered;
579    }
580
581    @Override
582    public List<ContextDefinition<?>> registeredContextTypes() {
583        return PCollections.asVector(this.contextTypes.values());
584    }
585
586    @Override
587    public @Nullable ContextDefinition<?> contextDefinition(final String definitionKey, final boolean allowFallbacks) {
588        @Nullable ContextDefinition<?> ret = this.contextTypes.get(definitionKey);
589        if (ret == null && allowFallbacks) {
590            ContextDefinition<?> fallback = new SimpleContextDefinition.Fallback(definitionKey);
591            ret = this.contextTypes.putIfAbsent(definitionKey, fallback);
592            if (ret == null) {
593                ret = fallback;
594            }
595        }
596        return ret;
597    }
598}