/** * PermissionsEx * Copyright (C) zml and PermissionsEx contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ninja.leaping.permissionsex.sponge; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.inject.Inject; import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.commented.CommentedConfigurationNode; import ninja.leaping.configurate.loader.ConfigurationLoader; import ninja.leaping.configurate.yaml.YAMLConfigurationLoader; import ninja.leaping.permissionsex.ImplementationInterface; import ninja.leaping.permissionsex.PermissionsEx; import ninja.leaping.permissionsex.config.FilePermissionsExConfiguration; import ninja.leaping.permissionsex.data.ImmutableSubjectData; import ninja.leaping.permissionsex.exception.PEBKACException; import ninja.leaping.permissionsex.logging.TranslatableLogger; import ninja.leaping.permissionsex.subject.SubjectType; import ninja.leaping.permissionsex.util.command.CommandContext; import ninja.leaping.permissionsex.util.command.CommandException; import ninja.leaping.permissionsex.util.command.CommandExecutor; import ninja.leaping.permissionsex.util.command.CommandSpec; import ninja.leaping.permissionsex.util.command.Commander; import org.slf4j.Logger; import org.spongepowered.api.Game; import org.spongepowered.api.command.CommandSource; import org.spongepowered.api.config.ConfigDir; import org.spongepowered.api.config.DefaultConfig; import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.game.GameReloadEvent; import org.spongepowered.api.event.game.state.GamePreInitializationEvent; import org.spongepowered.api.event.game.state.GameStoppedServerEvent; import org.spongepowered.api.event.network.ClientConnectionEvent; import org.spongepowered.api.plugin.Plugin; import org.spongepowered.api.plugin.PluginContainer; import org.spongepowered.api.scheduler.Scheduler; import org.spongepowered.api.service.ServiceManager; import org.spongepowered.api.service.permission.PermissionDescription; import org.spongepowered.api.service.permission.PermissionService; import org.spongepowered.api.service.permission.Subject; import org.spongepowered.api.service.permission.SubjectCollection; import org.spongepowered.api.service.context.ContextCalculator; import org.spongepowered.api.service.sql.SqlService; import org.spongepowered.api.util.annotation.NonnullByDefault; import javax.annotation.Nullable; import javax.sql.DataSource; import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.sql.SQLException; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.function.Function; import static ninja.leaping.permissionsex.sponge.SpongeTranslations.t; import static ninja.leaping.permissionsex.util.command.args.GenericArguments.string; /** * PermissionsEx plugin */ @NonnullByDefault @Plugin(id = PomData.ARTIFACT_ID, name = PomData.NAME, version = PomData.VERSION) public class PermissionsExPlugin implements PermissionService, ImplementationInterface { private Optional<SqlService> sql; private Scheduler scheduler; @Inject private ServiceManager services; private final TranslatableLogger logger; @Inject @ConfigDir(sharedRoot = false) private Path configDir; @Inject @DefaultConfig(sharedRoot = false) private ConfigurationLoader<CommentedConfigurationNode> configLoader; @Inject private Game game; private PermissionsEx manager; private final List<ContextCalculator<Subject>> contextCalculators = new CopyOnWriteArrayList<>(); private final ConcurrentMap<String, Function<String, Optional<CommandSource>>> commandSourceProviders = new ConcurrentHashMap<>(); private final LoadingCache<String, PEXSubjectCollection> subjectCollections = CacheBuilder.newBuilder().build(new CacheLoader<String, PEXSubjectCollection>() { @Override public PEXSubjectCollection load(String type) throws Exception { return new PEXSubjectCollection(type, PermissionsExPlugin.this); } }); private PEXSubject defaults; private final PEXContextCalculator contextCalculator = new PEXContextCalculator(); private final Map<String, PEXPermissionDescription> descriptions = new ConcurrentHashMap<>(); private Executor spongeExecutor = runnable -> scheduler .createTaskBuilder() .async() .execute(runnable) .submit(PermissionsExPlugin.this); private Timings timings; @Inject PermissionsExPlugin(Logger logger) { this.logger = TranslatableLogger.forLogger(logger); } @Listener public void onPreInit(GamePreInitializationEvent event) throws PEBKACException { this.timings = new Timings(this); logger.info(t("Pre-init of %s v%s", PomData.NAME, PomData.VERSION)); sql = services.provide(SqlService.class); scheduler = game.getScheduler(); try { convertFromBukkit(); convertFromLegacySpongeName(); Files.createDirectories(configDir); this.manager = new PermissionsEx(FilePermissionsExConfiguration.fromLoader(this.configLoader), this); } catch (Exception e) { throw new RuntimeException(t("Error occurred while enabling %s", PomData.NAME).translateFormatted(logger.getLogLocale()), e); } defaults = getSubjects(PermissionsEx.SUBJECTS_DEFAULTS).get(PermissionsEx.SUBJECTS_DEFAULTS); setCommandSourceProvider(getUserSubjects(),name -> { UUID uid; try { uid = UUID.fromString(name); } catch (IllegalArgumentException ex) { return Optional.empty(); } // Yeah, java generics are stupid return (Optional) game.getServer().getPlayer(uid); }); setCommandSourceProvider(getSubjects(PermissionService.SUBJECTS_SYSTEM), input -> { switch (input) { case "Server": return Optional.of(game.getServer().getConsole()); case "RCON": break; } return Optional.empty(); }); registerContextCalculator(contextCalculator); getManager().getSubjects(SUBJECTS_USER).setTypeInfo(new UserSubjectTypeDescription(SUBJECTS_USER, this)); registerFakeOpCommand("op", "minecraft.command.op"); registerFakeOpCommand("deop", "minecraft.command.deop"); // Registering the PEX service *must* occur after the plugin has been completely initialized if (!services.isRegistered(PermissionService.class)) { services.setProvider(this, PermissionService.class, this); } else { manager.close(); throw new PEBKACException(t("Your appear to already be using a different permissions plugin!")); } } private void registerFakeOpCommand(String alias, String permission) { registerCommand(CommandSpec.builder() .setAliases(alias) .setPermission(permission) .setDescription(t("A dummy replacement for vanilla's operator commands")) .setArguments(string(t("user"))) .setExecutor(new CommandExecutor() { @Override public <TextType> void execute(Commander<TextType> src, CommandContext ctx) throws CommandException { throw new CommandException(t("PermissionsEx replaces the server op/deop commands. Use PEX commands to manage permissions instead!")); } }) .build()); } @Listener public void cacheUserAsync(ClientConnectionEvent.Auth event) { try { getManager().getSubjects(PermissionsEx.SUBJECTS_USER).get(event.getProfile().getUniqueId().toString()); } catch (Exception e) { logger.warn(t("Error while loading data for user %s/%s during prelogin: %s", event.getProfile().getName(), event.getProfile().getUniqueId().toString(), e.getMessage()), e); } } @Listener public void disable(GameStoppedServerEvent event) { logger.debug(t("Disabling %s", PomData.NAME)); PermissionsEx manager = this.manager; if (manager != null) { manager.close(); } } @Listener public void onReload(GameReloadEvent event) { if (this.manager != null) { this.manager.reload(); } } @Listener public void onPlayerJoin(final ClientConnectionEvent.Join event) { final String identifier = event.getTargetEntity().getIdentifier(); final SubjectType cache = getManager().getSubjects(PermissionsEx.SUBJECTS_USER); cache.isRegistered(identifier).thenAccept(registered -> { if (registered) { cache.persistentData().update(identifier, input -> { if (event.getTargetEntity().getName().equals(input.getOptions(PermissionsEx.GLOBAL_CONTEXT).get("name"))) { return input; } else { return input.setOption(PermissionsEx.GLOBAL_CONTEXT, "name", event.getTargetEntity().getName()); } }); } }); } @Listener public void onPlayerQuit(ClientConnectionEvent.Disconnect event) { getUserSubjects().uncache(event.getTargetEntity().getIdentifier()); } public Timings getTimings() { return timings; } private void convertFromBukkit() throws IOException { Path bukkitConfigPath = Paths.get("plugins/PermissionsEx"); if (Files.isDirectory(bukkitConfigPath) && isDirectoryEmpty(configDir)) { logger.info(t("Migrating configuration data from Bukkit")); Files.move(bukkitConfigPath, configDir, StandardCopyOption.REPLACE_EXISTING); } Path bukkitConfigFile = configDir.resolve("config.yml"); if (Files.exists(bukkitConfigFile)) { ConfigurationLoader<ConfigurationNode> yamlReader = YAMLConfigurationLoader.builder().setPath(bukkitConfigFile).build(); ConfigurationNode bukkitConfig = yamlReader.load(); configLoader.save(bukkitConfig); Files.move(bukkitConfigFile, configDir.resolve("config.yml.bukkit")); } } private void convertFromLegacySpongeName() throws IOException { Path oldPath = configDir.resolveSibling("ninja.leaping.permissionsex"); // Old plugin ID if (Files.exists(oldPath) && isDirectoryEmpty(configDir)) { Files.move(oldPath, configDir, StandardCopyOption.REPLACE_EXISTING); Files.move(configDir.resolve("ninja.leaping.permissionsex.conf"), configDir.resolve(PomData.ARTIFACT_ID + ".conf")); logger.info(t("Migrated legacy sponge config directory to new location. Configuration is now located in %s", configDir.toString())); } } private boolean isDirectoryEmpty(Path dir) throws IOException { if (Files.exists(dir)) { return true; } try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dir)) { return !dirStream.iterator().hasNext(); } } PermissionsEx getManager() { return this.manager; } @Override public PEXSubjectCollection getUserSubjects() { try { return subjectCollections.get(SUBJECTS_USER); } catch (ExecutionException e) { throw new RuntimeException(e); } } @Override public PEXSubjectCollection getGroupSubjects() { try { return subjectCollections.get(SUBJECTS_GROUP); } catch (ExecutionException e) { throw new RuntimeException(e); } } @Override public PEXSubject getDefaults() { return defaults; } @Override public PEXSubjectCollection getSubjects(String identifier) { Preconditions.checkNotNull(identifier, "identifier"); try { return subjectCollections.get(identifier); } catch (ExecutionException e) { getLogger().error(t("Unable to get subject collection for type %s", identifier), e); return null; } } @Override @SuppressWarnings("unchecked") // TODO: Get values from DataStore.getRegisteredTypes() public Map<String, SubjectCollection> getKnownSubjects() { return (Map) subjectCollections.asMap(); } @Override public void registerContextCalculator(ContextCalculator<Subject> calculator) { contextCalculators.add(calculator); } @Override public Optional<PermissionDescription.Builder> newDescriptionBuilder(Object instance) { Optional<PluginContainer> container = this.game.getPluginManager().fromInstance(instance); if (!container.isPresent()) { throw new IllegalArgumentException("Provided plugin did not have an associated plugin instance. Are you sure it's your plugin instance?"); } return Optional.of(new PEXPermissionDescription.Builder(container.get(), this)); } void registerDescription(final PEXPermissionDescription description, Map<String, Integer> ranks) { this.descriptions.put(description.getId(), description); final SubjectType coll = getManager().getSubjects(SUBJECTS_ROLE_TEMPLATE); for (final Map.Entry<String, Integer> rank : ranks.entrySet()) { try { coll.transientData().update(rank.getKey(), new Function<ImmutableSubjectData, ImmutableSubjectData>() { @Nullable @Override public ImmutableSubjectData apply(@Nullable ImmutableSubjectData input) { return Preconditions.checkNotNull(input).setPermission(PermissionsEx.GLOBAL_CONTEXT, description.getId(), rank.getValue()); } }).get(); } catch (InterruptedException | ExecutionException e) { throw Throwables.propagate(e); } } } @Override public Optional<PermissionDescription> getDescription(String s) { return Optional.ofNullable(this.descriptions.get(s)); } @Override public Collection<PermissionDescription> getDescriptions() { return ImmutableSet.<PermissionDescription>copyOf(this.descriptions.values()); } public List<ContextCalculator<Subject>> getContextCalculators() { return contextCalculators; } @Override public Path getBaseDirectory() { return configDir; } @Override public TranslatableLogger getLogger() { return logger; } @Override @Nullable public DataSource getDataSourceForURL(String url) { if (!sql.isPresent()) { return null; } try { return sql.get().getDataSource(this, url); } catch (SQLException e) { logger.error(t("Unable to get data source for jdbc url %s", url), e); return null; } } /** * Get an executor to run tasks asynchronously on. * * @return The async executor */ @Override public Executor getAsyncExecutor() { return this.spongeExecutor; } @Override public void registerCommand(CommandSpec command) { game.getCommandManager().register(this, new PEXSpongeCommand(command, this), command.getAliases()); } @Override public Set<CommandSpec> getImplementationCommands() { return ImmutableSet.of(); } @Override public String getVersion() { return PomData.VERSION; } Function<String, Optional<CommandSource>> getCommandSourceProvider(String subjectCollection) { return commandSourceProviders.getOrDefault(subjectCollection, k -> Optional.empty()); } public void setCommandSourceProvider(PEXSubjectCollection subjectCollection, Function<String, Optional<CommandSource>> provider) { commandSourceProviders.put(subjectCollection.getIdentifier(), provider); } public Iterable<PEXSubject> getAllActiveSubjects() { return Iterables.concat(Iterables.transform(subjectCollections.asMap().values(), PEXSubjectCollection::getActiveSubjects)); } public Game getGame() { return game; } }