Skip to content

Commit 7344e89

Browse files
committed
ENHANCEMENTS:
- New parameter "restrict_commands" in nodes.yaml to disable specific elevated commands for hosting situations. CHANGES: - Disabled several commands (see above), up- and downloads. BUGFIX: - WebService did not switch nodes properly on Master switches.
1 parent 65bac72 commit 7344e89

File tree

20 files changed

+146
-31
lines changed

20 files changed

+146
-31
lines changed

MULTINODE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,18 @@ that are named DCS.release_server on any of your nodes. This can be what you wan
175175
I would always recommend creating the node-specific version (ex: "Multi-Node-Config" above) to avoid confusion. That's
176176
what the bot will create during a default installation also.
177177
178+
## Running a node for another group
179+
To run a node where you want to run servers for another group, you can use the `restrict_commands` setting in your
180+
nodes.yaml. This will disable commands that can affect the integrity of your PC, like `/node shell`.
181+
This is recommended for nodes that are run by you, but Admin accesses are happening without your control.
182+
```yaml
183+
Node1: # node where I have full control
184+
# ...
185+
Node2: # node where I do not have full control
186+
restrict_commands: true
187+
# ...
188+
```
189+
178190
### Moving a Server from one Node / Instance to another
179191
Each server is loosely coupled to an instance on a node. You can migrate a server to another instance though, by using
180192
the `/server migrate` command.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ NODENAME: # this will usually be your hostname
346346
slow_system: false # Optional: if you are using a slower PC to run your servers, you should set this to true (default: false)
347347
use_upnp: true # The bot will auto-detect if there is a UPnP IGD available and configure this setting initially for you! If you do NOT want to use UPnP, even IF it is available, put this to false.
348348
nodestats: true # Enable/disable node statistics (database pool and event queue sizes), default: true
349+
restrict_commands: true # Disable commands that can affect the integrity of the server. Default: false (see MULTINODE.md)
349350
database: # Optional: It might be that you need to use different IPs to connect to the same database server. This is the place you could do that.
350351
url: postgres://USER:PASSWORD@DB-IP:DB-PORT/DB-NAME # The bot will auto-move the database password from here to a secret place and replace it with SECRET.
351352
pool_min: 5 # min size of the DB pool, default is 5

core/data/impl/nodeimpl.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,9 @@ def run_subprocess():
974974
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
975975
return proc.communicate(timeout=timeout)
976976

977+
if self.locals.get('restrict_commands', False):
978+
raise discord.app_commands.CheckFailure("Shell commands are restricted on this node!")
979+
977980
self.log.debug('Running shell-command: ' + cmd)
978981
try:
979982
stdout, stderr = await asyncio.to_thread(run_subprocess)

core/utils/discord.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"app_has_not_roles",
4848
"app_has_dcs_version",
4949
"cmd_has_roles",
50+
"restricted",
5051
"get_role_ids",
5152
"format_embed",
5253
"embed_to_text",
@@ -484,6 +485,10 @@ async def wrapper(interaction: Interaction):
484485
return cmd_has_roles
485486

486487

488+
def restricted(interaction: discord.Interaction) -> bool:
489+
return not interaction.client.node.locals.get('restrict_commands', False)
490+
491+
487492
def get_role_ids(plugin: Plugin, role_names) -> list[int]:
488493
role_ids = []
489494
if not isinstance(role_names, list):

plugins/admin/commands.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,11 @@ async def label_autocomplete(interaction: discord.Interaction, current: str) ->
8888
config = interaction.client.cogs['Admin'].get_config(server)
8989
choices: list[app_commands.Choice[str]] = [
9090
app_commands.Choice(name=x['label'], value=x['label']) for x in config['downloads']
91-
if ((not current or current.casefold() in x['label'].casefold()) and
92-
(not x.get('discord') or utils.check_roles(x['discord'], interaction.user)))
91+
if (
92+
(not current or current.casefold() in x['label'].casefold()) and
93+
(not x.get('discord') or utils.check_roles(x['discord'], interaction.user)) and
94+
(not x.get('restricted', False) or not server.node.locals.get('restrict_commands', False))
95+
)
9396
]
9497
return choices[:25]
9598
except Exception as ex:
@@ -131,6 +134,14 @@ async def file_autocomplete(interaction: discord.Interaction, current: str) -> l
131134
config = next(x for x in config['downloads'] if x['label'] == label)
132135
except StopIteration:
133136
return []
137+
138+
# check if we are allowed to display the list
139+
if (
140+
(config.get('discord') and not utils.check_roles(config['discord'], interaction.user)) or
141+
(config.get('restricted', False) and server.node.locals.get('restrict_commands', False))
142+
):
143+
return []
144+
134145
base_dir = utils.format_string(config['directory'], server=server)
135146
exp_base, file_list = await server.node.list_directory(base_dir, pattern=config['pattern'], traverse=True)
136147
choices: list[app_commands.Choice[str]] = [
@@ -357,6 +368,7 @@ async def bans(self, interaction: discord.Interaction, user: str):
357368

358369
@dcs.command(description=_('Update your DCS installations'))
359370
@app_commands.guild_only()
371+
@app_commands.check(utils.restricted)
360372
@utils.app_has_role('DCS Admin')
361373
@app_commands.describe(warn_time=_("Time in seconds to warn users before shutdown"))
362374
@app_commands.autocomplete(branch=get_dcs_branches)
@@ -435,6 +447,7 @@ async def update(self, interaction: discord.Interaction,
435447

436448
@dcs.command(name='install', description=_('Install modules in your DCS server'))
437449
@app_commands.guild_only()
450+
@app_commands.check(utils.restricted)
438451
@utils.app_has_role('Admin')
439452
@app_commands.autocomplete(module=available_modules_autocomplete)
440453
async def _install(self, interaction: discord.Interaction,
@@ -457,6 +470,7 @@ async def _install(self, interaction: discord.Interaction,
457470

458471
@dcs.command(name='uninstall', description=_('Uninstall modules from your DCS server'))
459472
@app_commands.guild_only()
473+
@app_commands.check(utils.restricted)
460474
@utils.app_has_role('Admin')
461475
@app_commands.autocomplete(module=installed_modules_autocomplete)
462476
async def _uninstall(self, interaction: discord.Interaction,
@@ -504,7 +518,10 @@ async def download(self, interaction: discord.Interaction,
504518
await interaction.response.defer(thinking=True, ephemeral=ephemeral)
505519
config = next(x for x in self.get_config(server)['downloads'] if x['label'] == what)
506520
# double-check if that user can really download these files
507-
if config.get('discord') and not utils.check_roles(config['discord'], interaction.user):
521+
if (
522+
(config.get('discord') and not utils.check_roles(config['discord'], interaction.user)) or
523+
(config.get('restricted', False) and server.node.locals.get('restrict_commands', False))
524+
):
508525
raise app_commands.CheckFailure()
509526
if what == 'Missions':
510527
base_dir = await server.get_missions_dir()
@@ -757,6 +774,7 @@ async def run_on_nodes(self, interaction: discord.Interaction, method: str, node
757774

758775
@node_group.command(description=_('Shuts a specific node down'))
759776
@app_commands.guild_only()
777+
@app_commands.check(utils.restricted)
760778
@utils.app_has_role('Admin')
761779
async def shutdown(self, interaction: discord.Interaction,
762780
node: Optional[app_commands.Transform[Node, utils.NodeTransformer]] = None):
@@ -767,6 +785,7 @@ async def shutdown(self, interaction: discord.Interaction,
767785

768786
@node_group.command(description=_('Restarts a specific node'))
769787
@app_commands.guild_only()
788+
@app_commands.check(utils.restricted)
770789
@utils.app_has_role('Admin')
771790
async def restart(self, interaction: discord.Interaction,
772791
node: Optional[app_commands.Transform[Node, utils.NodeTransformer]] = None):
@@ -862,6 +881,7 @@ async def _node_online(node_name: str):
862881

863882
@node_group.command(description=_('Upgrade DCSServerBot'))
864883
@app_commands.guild_only()
884+
@app_commands.check(utils.restricted)
865885
@utils.app_has_role('Admin')
866886
async def upgrade(self, interaction: discord.Interaction,
867887
node: Optional[app_commands.Transform[Node, utils.NodeTransformer]] = None):
@@ -887,6 +907,7 @@ async def upgrade(self, interaction: discord.Interaction,
887907

888908
@node_group.command(description=_('Run a shell command on a node'))
889909
@app_commands.guild_only()
910+
@app_commands.check(utils.restricted)
890911
@utils.app_has_role('Admin')
891912
async def shell(self, interaction: discord.Interaction,
892913
node: app_commands.Transform[Node, utils.NodeTransformer],
@@ -910,6 +931,7 @@ async def shell(self, interaction: discord.Interaction,
910931

911932
@node_group.command(description=_("Add/create an instance\n"))
912933
@app_commands.guild_only()
934+
@app_commands.check(utils.restricted)
913935
@utils.app_has_role('Admin')
914936
@app_commands.autocomplete(name=utils.InstanceTransformer(unused=True).autocomplete)
915937
@app_commands.describe(name=_("Either select an existing instance or enter the name of a new one"))
@@ -965,6 +987,7 @@ async def add_instance(self, interaction: discord.Interaction,
965987

966988
@node_group.command(description=_("Delete an instance\n"))
967989
@app_commands.guild_only()
990+
@app_commands.check(utils.restricted)
968991
@utils.app_has_role('Admin')
969992
async def delete_instance(self, interaction: discord.Interaction,
970993
node: app_commands.Transform[Node, utils.NodeTransformer],
@@ -1002,6 +1025,7 @@ async def delete_instance(self, interaction: discord.Interaction,
10021025

10031026
@node_group.command(description=_("Rename an instance\n"))
10041027
@app_commands.guild_only()
1028+
@app_commands.check(utils.restricted)
10051029
@utils.app_has_role('Admin')
10061030
async def rename_instance(self, interaction: discord.Interaction,
10071031
node: app_commands.Transform[Node, utils.NodeTransformer],
@@ -1044,6 +1068,7 @@ async def rename_instance(self, interaction: discord.Interaction,
10441068

10451069
@node_group.command(description=_("Shows CPU topology"))
10461070
@app_commands.guild_only()
1071+
@app_commands.check(utils.restricted)
10471072
@app_commands.check(lambda interaction: sys.platform == 'win32')
10481073
@utils.app_has_role('Admin')
10491074
async def cpuinfo(self, interaction: discord.Interaction,
@@ -1057,6 +1082,7 @@ async def cpuinfo(self, interaction: discord.Interaction,
10571082

10581083
@plug.command(name='install', description=_("Install Plugin"))
10591084
@app_commands.guild_only()
1085+
@app_commands.check(utils.restricted)
10601086
@app_commands.autocomplete(plugin=installable_plugins)
10611087
@utils.app_has_role('Admin')
10621088
async def _install(self, interaction: discord.Interaction, plugin: str):
@@ -1074,6 +1100,7 @@ async def _install(self, interaction: discord.Interaction, plugin: str):
10741100

10751101
@plug.command(name='uninstall', description=_("Uninstall Plugin"))
10761102
@app_commands.guild_only()
1103+
@app_commands.check(utils.restricted)
10771104
@app_commands.autocomplete(plugin=uninstallable_plugins)
10781105
@utils.app_has_role('Admin')
10791106
async def _uninstall(self, interaction: discord.Interaction, plugin: str):
@@ -1091,6 +1118,7 @@ async def _uninstall(self, interaction: discord.Interaction, plugin: str):
10911118

10921119
@plug.command(description=_('Reload Plugin'))
10931120
@app_commands.guild_only()
1121+
@app_commands.check(utils.restricted)
10941122
@utils.app_has_role('Admin')
10951123
@app_commands.autocomplete(plugin=plugins_autocomplete)
10961124
async def reload(self, interaction: discord.Interaction, plugin: Optional[str]):
@@ -1220,7 +1248,7 @@ async def on_message(self, message: discord.Message):
12201248
# read the default config if there is any
12211249
config = self.get_config().get('uploads', {})
12221250
# check if upload is enabled
1223-
if not config.get('enabled', True):
1251+
if not config.get('enabled', True) or self.node.locals.get('restricted'):
12241252
return
12251253
# check if the user has the correct role to upload, defaults to Admin
12261254
if not utils.check_roles(config.get('discord', self.bot.roles['Admin']), message.author):

plugins/admin/schemas/admin_schema.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ schema;downloads:
1414
- type: text
1515
nullable: false
1616
audit: {type: bool, nullable: false}
17+
restricted: {type: bool, nullable: false}
1718

1819
schema;uploads:
1920
type: map

plugins/backup/commands.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def read_locals(self) -> dict:
7272

7373
@command(description=_('Backup your data'))
7474
@app_commands.guild_only()
75+
@app_commands.check(utils.restricted)
7576
@utils.app_has_role('Admin')
7677
@app_commands.autocomplete(what=backup_autocomplete)
7778
async def backup(self, interaction: discord.Interaction, node: app_commands.Transform[Node, utils.NodeTransformer],
@@ -95,6 +96,7 @@ async def backup(self, interaction: discord.Interaction, node: app_commands.Tran
9596

9697
@command(description=_('Recover your data from an existing backup'))
9798
@app_commands.guild_only()
99+
@app_commands.check(utils.restricted)
98100
@utils.app_has_role('Admin')
99101
@app_commands.autocomplete(what=backup_autocomplete)
100102
@app_commands.autocomplete(date=date_autocomplete)

plugins/lotatc/commands.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ async def get_server(self, message: discord.Message) -> Optional[Server]:
6868

6969
@lotatc.command(description=_('Update LotAtc'))
7070
@app_commands.guild_only()
71+
@app_commands.check(utils.restricted)
7172
@utils.app_has_role('DCS Admin')
7273
async def update(self, interaction: discord.Interaction,
7374
server: app_commands.Transform[Server, utils.ServerTransformer(
@@ -125,6 +126,7 @@ async def _configure(self, interaction: discord.Interaction,
125126

126127
@lotatc.command(description=_('Configure LotAtc'))
127128
@app_commands.guild_only()
129+
@app_commands.check(utils.restricted)
128130
@utils.app_has_role('DCS Admin')
129131
async def configure(self, interaction: discord.Interaction,
130132
server: app_commands.Transform[Server, utils.ServerTransformer(status=[Status.SHUTDOWN])],

plugins/missionstats/commands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ async def history(self, interaction: discord.Interaction,
253253
return
254254

255255
start = datetime.strptime(start, '%Y-%m-%d') if start else (datetime.now() - timedelta(days=30)).date()
256-
end = datetime.strptime(end, '%Y-%m-%d') if end else datetime.now().date()
256+
end = datetime.strptime(end, '%Y-%m-%d') if end else datetime.now()
257257

258258
ephemeral = not utils.get_ephemeral(interaction)
259259
# noinspection PyUnresolvedReferences
@@ -283,7 +283,7 @@ async def history(self, interaction: discord.Interaction,
283283
ephemeral=ephemeral)
284284
return
285285

286-
# Create in-memory binary stream
286+
# Create an in-memory binary stream
287287
excel_binary = BytesIO()
288288

289289
# Define the desired column order

plugins/modmanager/commands.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ async def rename(self, conn: psycopg.AsyncConnection, old_name: str, new_name: s
153153

154154
@mods.command(description=_('manage mods'))
155155
@app_commands.guild_only()
156+
@app_commands.check(utils.restricted)
156157
@utils.app_has_roles(['Admin'])
157158
async def manage(self, interaction: discord.Interaction,
158159
server: app_commands.Transform[Server, utils.ServerTransformer(
@@ -365,6 +366,7 @@ async def cancel(derived, _: discord.Interaction):
365366

366367
@mods.command(name="install", description=_('Install mods'))
367368
@app_commands.guild_only()
369+
@app_commands.check(utils.restricted)
368370
@utils.app_has_roles(['Admin'])
369371
@app_commands.autocomplete(mod=available_mods_autocomplete)
370372
@app_commands.autocomplete(version=available_versions_autocomplete)
@@ -415,6 +417,7 @@ async def _install(self, interaction: discord.Interaction,
415417

416418
@mods.command(description=_('Uninstall mods'))
417419
@app_commands.guild_only()
420+
@app_commands.check(utils.restricted)
418421
@utils.app_has_roles(['Admin'])
419422
@app_commands.autocomplete(mod=installed_mods_autocomplete)
420423
async def uninstall(self, interaction: discord.Interaction,
@@ -464,6 +467,7 @@ async def _list(self, interaction: discord.Interaction,
464467

465468
@mods.command(description=_('Download a mod'))
466469
@app_commands.guild_only()
470+
@app_commands.check(utils.restricted)
467471
@utils.app_has_roles(['Admin'])
468472
@app_commands.describe(url=_("GitHub repo link or download URL"))
469473
@app_commands.autocomplete(version=repo_version_autocomplete)

0 commit comments

Comments
 (0)