Permissions framework + stub DB

keep-around/d31701866686f66088b78de2e29736ae36e55a68
Pingex aka Raphaël 9 years ago
parent 073d2455cb
commit 38ccb6e9f6

@ -5,6 +5,7 @@ import net.pingex.dcf.commands.InternalCommands;
import net.pingex.dcf.core.Configuration;
import net.pingex.dcf.core.GatewayConnectionsManager;
import net.pingex.dcf.modularity.PluginLoader;
import net.pingex.dcf.permissions.PermissionsCommands;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import sx.blah.discord.api.ClientBuilder;
@ -44,6 +45,7 @@ public class DiscordCommandableFramework
// Register internal commands
new InternalCommands().getCommands().forEach(CommandRegistry::registerCommand);
new PermissionsCommands().getCommands().forEach(CommandRegistry::registerCommand);
// Run plugins
PluginLoader.getInstance().bulkRunPlugins();

@ -1,5 +1,6 @@
package net.pingex.dcf.commands;
import net.pingex.dcf.permissions.DefaultPermission;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -36,4 +37,9 @@ public @interface AnnotatedCommand
* Command usage help
*/
String usage() default Command.Defaults.USAGE;
/**
* Default permission, ie. when the permissions provider doesn't return anything.
*/
DefaultPermission defaultPermission() default DefaultPermission.EVERYONE;
}

@ -1,5 +1,6 @@
package net.pingex.dcf.commands;
import net.pingex.dcf.permissions.DefaultPermission;
import sx.blah.discord.handle.impl.events.MessageReceivedEvent;
import java.lang.reflect.Method;
import java.util.Arrays;
@ -36,6 +37,11 @@ public abstract class Command implements ICommandExecutor
*/
private String usage;
/**
* Default permission, ie. when the permissions provider doesn't return anything.
*/
private DefaultPermission defaultPermission;
/**
* Basic constructor.
* @param name Name of the command
@ -43,14 +49,16 @@ public abstract class Command implements ICommandExecutor
* @param description Description of the command
* @param isEnabled Is the command enabled ?
* @param usage Command usage help
* @param defaultPermission Default permission, ie. when the permissions provider doesn't return anything.
*/
public Command(String name, List<String> aliases, String description, boolean isEnabled, String usage)
public Command(String name, List<String> aliases, String description, boolean isEnabled, String usage, DefaultPermission defaultPermission)
{
this.name = name;
this.aliases = aliases;
this.description = description;
this.isEnabled = isEnabled;
this.usage = usage;
this.defaultPermission = defaultPermission;
}
/**
@ -62,6 +70,7 @@ public abstract class Command implements ICommandExecutor
public static final String DESCRIPTION = "No command description provided.";
public static final boolean IS_ENABLED = true;
public static final String USAGE = "No command usage provided.";
public static final DefaultPermission DEFAULT_PERMISSION = DefaultPermission.EVERYONE;
}
/**
@ -94,6 +103,11 @@ public abstract class Command implements ICommandExecutor
*/
private String usage = Defaults.USAGE;
/**
* Default permission, ie. when the permissions provider doesn't return anything.
*/
private DefaultPermission defaultPermission = Defaults.DEFAULT_PERMISSION;
public Builder(String name)
{
this.name = name;
@ -133,6 +147,12 @@ public abstract class Command implements ICommandExecutor
return this;
}
public Builder defaultPermission(DefaultPermission defaultPermission)
{
this.defaultPermission = defaultPermission;
return this;
}
/**
* Build a new Command using a supplied executor.
* @param toExecute The body of the command.
@ -140,7 +160,7 @@ public abstract class Command implements ICommandExecutor
*/
public Command build(ICommandExecutor toExecute)
{
return new Command(name, aliases, description, isEnabled, usage)
return new Command(name, aliases, description, isEnabled, usage, defaultPermission)
{
@Override
public void execute(MessageReceivedEvent event, List<String> arguments) throws Throwable
@ -158,7 +178,7 @@ public abstract class Command implements ICommandExecutor
*/
public Command build(Method target, Object invokable)
{
return new Command(name, aliases, description, isEnabled, usage)
return new Command(name, aliases, description, isEnabled, usage, defaultPermission)
{
@Override
public void execute(MessageReceivedEvent event, List<String> arguments) throws Throwable
@ -191,6 +211,7 @@ public abstract class Command implements ICommandExecutor
.description(meta.description())
.enabled(meta.isEnabled())
.usage(meta.usage())
.defaultPermission(meta.defaultPermission())
.build(target, invokable);
}
@ -219,6 +240,11 @@ public abstract class Command implements ICommandExecutor
return usage;
}
public DefaultPermission getDefaultPermission()
{
return defaultPermission;
}
public void setEnabled(boolean enabled)
{
isEnabled = enabled;

@ -5,6 +5,7 @@ import net.pingex.dcf.commands.parser.ICommandParser;
import net.pingex.dcf.commands.parser.ParserException;
import net.pingex.dcf.core.Configuration;
import net.pingex.dcf.modularity.PluginWrapper;
import net.pingex.dcf.permissions.PermissionsHandler;
import net.pingex.dcf.util.DiscordInteractionsUtil;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.apache.logging.log4j.LogManager;
@ -66,6 +67,15 @@ public class CommandHandler
return;
}
// Authorize
boolean canRun = PermissionsHandler.canRunCommand(event.getMessage(), targetCommand.get());
if(!canRun)
{
LOGGER.debug("User {} attempted to run a forbidden command !", event.getMessage().getAuthor().getID());
DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), "Access denied.");
return;
}
// Run it
threadPool.submit(() ->
{

@ -1,6 +1,7 @@
package net.pingex.dcf.commands;
import net.pingex.dcf.core.Configuration;
import net.pingex.dcf.permissions.DefaultPermission;
import net.pingex.dcf.util.ArgumentParser;
import net.pingex.dcf.util.DiscordInteractionsUtil;
import org.apache.commons.lang3.StringUtils;
@ -28,6 +29,7 @@ public class InternalCommands implements IWithCommands
private static final String DESCRIPTION = "List all available commands.";
private static final boolean IS_ENABLED = true;
private static final String USAGE = Configuration.COMMAND_PREFIX + NAME + " <page>";
private static final DefaultPermission DEFAULT_PERMISSION = DefaultPermission.EVERYONE;
/**
* How many commands should be displayed on each page
@ -38,7 +40,7 @@ public class InternalCommands implements IWithCommands
private ListCommand()
{
super(NAME, ALIASES, DESCRIPTION, IS_ENABLED, USAGE);
super(NAME, ALIASES, DESCRIPTION, IS_ENABLED, USAGE, DEFAULT_PERMISSION);
}
@Override
@ -106,12 +108,13 @@ public class InternalCommands implements IWithCommands
private static final String DESCRIPTION = "Gives the usage of a command.";
private static final boolean IS_ENABLED = true;
private static final String USAGE = Configuration.COMMAND_PREFIX + NAME + " <command>";
private static final DefaultPermission DEFAULT_PERMISSION = DefaultPermission.EVERYONE;
static final UsageCommand INSTANCE = new UsageCommand();
private UsageCommand()
{
super(NAME, ALIASES, DESCRIPTION, IS_ENABLED, USAGE);
super(NAME, ALIASES, DESCRIPTION, IS_ENABLED, USAGE, DEFAULT_PERMISSION);
}
@Override

@ -0,0 +1,49 @@
package net.pingex.dcf.permissions;
import net.pingex.dcf.core.Configuration;
import sx.blah.discord.handle.obj.IMessage;
import java.util.function.Predicate;
/**
* Default behavior when a permissions provider doesn't return `true` or `false`
*/
public enum DefaultPermission implements Predicate<IMessage>
{
/**
* Everyone is allowed to run the command.
*/
EVERYONE(iMessage -> true),
/**
* Only the guild owner is allowed to run the command.
*/
GUILD_OWNER(iMessage -> iMessage.getAuthor().getID().equals(iMessage.getGuild().getOwnerID())),
/**
* Only the bot owner is allowed to run the command.
*/
BOT_OWNER(iMessage -> iMessage.getAuthor().getID().equals(Configuration.BOT_OWNER)),
/**
* Guild Owner x Bot Owner
*/
ANY_OWNER(iMessage -> GUILD_OWNER.test(iMessage) || BOT_OWNER.test(iMessage)),
/**
* Nobody is allowed to run the command
*/
NONE(iMessage -> false);
DefaultPermission(Predicate<IMessage> predicate)
{
this.predicate = predicate;
}
private Predicate<IMessage> predicate;
@Override
public boolean test(IMessage o)
{
return predicate.test(o);
}
}

@ -0,0 +1,24 @@
package net.pingex.dcf.permissions;
import net.pingex.dcf.commands.Command;
import sx.blah.discord.handle.obj.IGuild;
import sx.blah.discord.handle.obj.IRole;
import sx.blah.discord.handle.obj.IUser;
/**
* The one and only permissions provider for now.
*/
public class DefaultPermissionsProvider implements ICommandPermissionsProvider
{
@Override
public Boolean validateUser(IGuild guild, IUser user, Command command)
{
return null;
}
@Override
public Boolean validateGroup(IRole role, Command command)
{
return null;
}
}

@ -0,0 +1,34 @@
package net.pingex.dcf.permissions;
import net.pingex.dcf.commands.Command;
import sx.blah.discord.handle.obj.IGuild;
import sx.blah.discord.handle.obj.IRole;
import sx.blah.discord.handle.obj.IUser;
/**
* Provider of permissions, ie. the proxy between {@code PermissionsHandler} and a data storage.
*
* The validator will successively test the following until something other than `null` is returned.
* - Individual user permission using {@code validateUser()}
* - The user's role using {@code validateGroup()}
* - Default behavior
*/
public interface ICommandPermissionsProvider
{
/**
* Validate individual user permission.
* @param guild Guild where command is executed. `null` = globally
* @param user Target user
* @param command Command to test
* @return `true` if the user is able to run the command, `false` if he isn't able to, `null` if there is no answer.
*/
Boolean validateUser(IGuild guild, IUser user, Command command);
/**
* Validate a role's ability to run a command.
* @param role Target role
* @param command Command to test
* @return `true` if the role is able to run the command, `false` if he isn't able to, `null` if there is no answer.
*/
Boolean validateGroup(IRole role, Command command);
}

@ -0,0 +1,96 @@
package net.pingex.dcf.permissions;
import net.pingex.dcf.commands.Command;
import net.pingex.dcf.commands.CommandRegistry;
import net.pingex.dcf.commands.IWithCommands;
import net.pingex.dcf.util.DiscordInteractionsUtil;
import sx.blah.discord.handle.impl.events.MessageReceivedEvent;
import sx.blah.discord.handle.obj.IUser;
import java.util.*;
import java.util.regex.Pattern;
/**
* Commands for the permissions package.
*/
public class PermissionsCommands implements IWithCommands
{
@Override
public Set<Command> getCommands()
{
return new HashSet<>(Arrays.asList(
IsAllowedCommand
));
}
private static final Command IsAllowedCommand =
new Command.Builder("canRun")
.description("Tells whether target user is allowed to run the command.")
.enabled(true)
.usage("Command <User#Disc|ID> [verbose]")
.defaultPermission(DefaultPermission.BOT_OWNER)
.build(PermissionsCommands::isAllowedImpl);
private static Pattern usernamePattern = Pattern.compile("^.*#\\d{4}$");
private static Pattern idPattern = Pattern.compile("^\\d{18}$");
private static void isAllowedImpl(MessageReceivedEvent event, List<String> arguments)
{
// Parameters
Command target = null;
IUser userToLookup = null;
boolean verbose = false;
// Argchk size
if(arguments.size() != 2 && arguments.size() != 3)
{
DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), "Invalid arguments.");
return;
}
// Argchk#1 - valid command
Optional<Command> uncheckedTarget = CommandRegistry.getCommandOrAliasByName(arguments.get(0));
if(!uncheckedTarget.isPresent()) // Command existence
{
DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), "Target command not found.");
return;
}
else target = uncheckedTarget.get();
// Argchk#2 - uid OR username#1234 OR "me"
if(idPattern.matcher(arguments.get(1)).matches()) // uid
{
userToLookup = event.getClient().getUserByID(arguments.get(1));
}
else if(usernamePattern.matcher(arguments.get(1)).matches()) // username#1234
{
for(IUser iUser : event.getClient().getUsers()) // Greedy
if(arguments.get(1).equalsIgnoreCase(iUser.getName() + "#" + iUser.getDiscriminator()))
{
userToLookup = iUser;
break; // Limit greediness
}
}
else if(arguments.get(1).equalsIgnoreCase("me")) // me
{
userToLookup = event.getMessage().getAuthor();
}
else // ?
{
DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), "Invalid arguments.");
return;
}
if(userToLookup == null)
{
DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), "User not found.");
return;
}
// Argchk#3 - verbosity
if(arguments.size() == 3) verbose = true;
// ========================================
DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), "STUB: Checking perms for command " + target.getName() + " and user " + userToLookup.getName());
}
}

@ -0,0 +1,69 @@
package net.pingex.dcf.permissions;
import net.pingex.dcf.commands.Command;
import sx.blah.discord.handle.obj.IGuild;
import sx.blah.discord.handle.obj.IMessage;
import sx.blah.discord.handle.obj.IRole;
import sx.blah.discord.handle.obj.IUser;
import java.util.Collections;
import java.util.List;
/**
* Landing class of this package.
*/
public class PermissionsHandler
{
private static final ICommandPermissionsProvider CURRENT_PROVIDER = new DefaultPermissionsProvider();
/**
* Tells whether someone can run requested command.
*
* The validator will successively test the following until something other than `null` is returned.
* - Individual user permission on the called guild using {@code ICommandPermissionsProvider.validateUser()}
* - Individual user permission globally using {@code ICommandPermissionsProvider.validateUser()}
* - The user's role list using {@code ICommandPermissionsProvider.validateGroup()}
* - Default behavior included in a Command object
*
* @param request Request message, includes author, guild, and so on.
* @param command Requested command, as parsed by {@code CommandHandler}
* @return `true` if request is granted, `false` if denied.
*/
public static boolean canRunCommand(IMessage request, Command command)
{
IUser requestAuthor = request.getAuthor();
IGuild originatedGuild;
List<IRole> rolesForUser;
try
{
originatedGuild = request.getGuild();
rolesForUser = requestAuthor.getRolesForGuild(originatedGuild);
}
catch(UnsupportedOperationException uoe) // DM
{
originatedGuild = null;
rolesForUser = Collections.emptyList();
}
// First check: user permission for guild, skipped if DM
if(originatedGuild != null)
{
Boolean canUserGuildRun = CURRENT_PROVIDER.validateUser(originatedGuild, requestAuthor, command);
if(canUserGuildRun != null) return canUserGuildRun;
}
// Second check: user permission globally
Boolean canUserRun = CURRENT_PROVIDER.validateUser(null, requestAuthor, command);
if(canUserRun != null) return canUserRun;
// Third check: user role permissions, auto-skipped if DM
for(IRole i : rolesForUser)
{
Boolean canRoleRun = CURRENT_PROVIDER.validateGroup(i, command);
if(canRoleRun != null) return canRoleRun;
}
// Fourth check: default behavior
return command.getDefaultPermission().test(request);
}
}

@ -0,0 +1,4 @@
/**
* This package contains everything related to permissions and the ability for an user to run something.
*/
package net.pingex.dcf.permissions;