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}