Skip to content
This repository has been archived by the owner on Jul 7, 2024. It is now read-only.

Commit

Permalink
core: added permissions framework (#16)
Browse files Browse the repository at this point in the history
* core: added permissions framework

* partial config and etc

* permissions: review improvements

* permissions: style fix

* permission improvements

* default config: more improvements

* permissions: fixed submodule permissions bug

---------

Co-authored-by: zleyyij <[email protected]>
  • Loading branch information
zleyyij and zleyyij authored Aug 26, 2023
1 parent e8ca985 commit de3f238
Show file tree
Hide file tree
Showing 4 changed files with 502 additions and 30 deletions.
45 changes: 45 additions & 0 deletions config.default.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,51 @@

// The key for each module should match the one specified in the root module initialization
// if the `enabled` key is missing, the extension will be disabled by default, and a warning issued
/*
Here's an example permissions config and boilerplate:
You can use this by setting the `permissions` key of each extension
to an object like this. It's not required for any of these keys to be set.
```
"permissions": {
// a list of all possible permission restrictions
"requiredPerms": ["kick", "ban", "timeout", "manage_roles", "administrator"],
// **ALL IDs MUST BE STRINGS (not numbers)**
"allowed": {
// list of user IDs
"users": [],
// list of role IDs (or names? needs to be discussed)
"roles": [],
// list of channel IDs
"channels": [],
// a list of category IDs
"categories": [],
},
"denied": {
// a list of user IDs
"users": [],
// a list of role IDs
"roles": [],
// a list of channel IDs
"channels": [],
// a list of category IDs.
"categories": [],
}
// if you also want to restrict permissions per submodule, it can be done as below
"submodulePermissions": {
// each key in this object is the name of a submodule, so for a command like `/factoid all`,
// you could restrict it with a key named `"all"`.
"<submodule_name>": {
// the same structure can be nested
"requiredPerms": [],
// you can also nest submodulePermissions to restrict a command 3 layers deep, like "/factoid all html"
"submodulePermissions: {
"<submodule_name>": {}
}
}
}
}
```
*/
"modules": {
"logging": {
"enabled": true,
Expand Down
1 change: 0 additions & 1 deletion docs/arch/ADR_03.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ I can't think of a great way to handle permissions for subcommands. I thought of
}
}
}

}
```

Expand Down
53 changes: 52 additions & 1 deletion src/core/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
registerSlashCommandSet,
replyToInteraction,
} from './slash_commands.js';
import {checkInteractionAgainstPermissionConfig} from './permissions.js';

// load the config from config.default.jsonc
botConfig.readConfigFromFileSystem();
Expand Down Expand Up @@ -83,7 +84,7 @@ async function importModulesFromFile(path: string): Promise<void> {
* Start an event listener that executes received slash commands
* https://discordjs.guide/creating-your-bot/command-handling.html#receiving-command-interactions */
function listenForSlashCommands() {
client.on(Events.InteractionCreate, interaction => {
client.on(Events.InteractionCreate, async interaction => {
if (!interaction.isChatInputCommand()) {
return;
}
Expand All @@ -101,6 +102,52 @@ function listenForSlashCommands() {
}
const resolutionResult = resolveModule(commandPath);
if (resolutionResult.foundModule !== null) {
// validate permissions
// if a permission config was not defined, treat it as empty
const permissionConfig =
resolutionResult.foundModule.config.permissions ?? {};
// this returns a list of reasons not to run the command, so if the list is empty,
// continue with execution
let deniedReasons: string[] = checkInteractionAgainstPermissionConfig(
interaction,
permissionConfig
);
// it's also possible to specify permissions per-submodule, and per submodule group
// checking 2 layers deep, eg `/foo bar`
if (resolutionResult.modulePath.length === 2) {
const submodulePermissionResults =
checkInteractionAgainstPermissionConfig(
interaction,
permissionConfig.submodulePermissions[
resolutionResult.modulePath[1]
] ?? {}
);
deniedReasons = deniedReasons.concat(submodulePermissionResults);
}

// 3 layers deep, EG `/foo bar bat`
if (resolutionResult.modulePath.length === 3) {
const subSubmodulePermissionResults =
checkInteractionAgainstPermissionConfig(
interaction,
permissionConfig.submodulePermissions[
resolutionResult.modulePath[1]
].submodulePermissions[resolutionResult.modulePath[2]]
);
deniedReasons = deniedReasons.concat(subSubmodulePermissionResults);
}

if (deniedReasons.length > 0) {
await replyToInteraction(interaction, {
embeds: [
embed.errorEmbed(
'You are unable to execute this command for the following reasons:\n- ' +
deniedReasons.join('\n- ')
),
],
});
return;
}
executeModule(resolutionResult.foundModule, interaction);
}
});
Expand Down Expand Up @@ -220,6 +267,8 @@ async function executeModule(
return;
}
}

// next figure out where the correct options are located, to pass to the module
// could be considered for minor optimizations
let options: CommandInteractionOption[];
// though a bit odd, the options are actually located at a different place in the object
Expand All @@ -230,6 +279,8 @@ async function executeModule(
} else {
options = Array.from(interaction.options.data);
}

// execute the command
// There may be possible minor perf/mem overhead from calling Array.from to un-readonly the array,
module
.executeCommand(Array.from(options), interaction)
Expand Down
Loading

0 comments on commit de3f238

Please sign in to comment.