Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions nautobot_chatops/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@

from .choices import AccessGrantTypeChoices, CommandStatusChoices, CommandTokenPlatformChoices

import re


class QuerySetRegexMatch(models.QuerySet):
"""Customized QuerySet to match string agains regexes stored in database.

Return a list of matched records.
"""

def match_re(self, value):
records = self.all()

results = []
for row in records:
if row.subcommand == "*": # skip as it is not a valid regex expression and would raise exception
continue
if re.match(row.subcommand, value):
results.append(row.subcommand)
return results
Comment on lines +24 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm definitely a bit concerned about potential performance impact of this design. It should be possible to do this as an actual DB query, rather than querying for all() and then iterating through the results, though from some quick Googling it looks like custom SQL might be needed to make that possible...

Copy link
Author

@david-kn david-kn Feb 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HI Matthew, thanks for your response and I understand this concern and agree with it.

I'm not familiar with with your code base and didn't want to dig to deep to explore (and reinvent the wheel) the best possible way to interconnect your Django with queries / SQL queries. I rather wanted to kick off this initiative and show that however this might be not that elegant, the update wouldn't have to be extensive to add this neat feature.

Probably some .raw() method to run directly SQL query could help with this (?) as I don't think that django libs would allow such a query. On the other hand, don't you mind writing and mixing SQL queries into your otherwise python code?

On the other hand, amount of these records should be always very limited so the performance should have not been really impacted? But I totally understand and agree that this (performance) aspect as well as conceptual perspective should be always considered and questioned.

I'd leave the final approach to you as your team is in owner/maintainer role :-)



class CommandLog(BaseModel):
"""Record of a single fully-executed Nautobot command.
Expand Down Expand Up @@ -68,6 +88,7 @@ class AccessGrant(BaseModel, ChangeLoggedModel):
max_length=64,
help_text="Enter <tt>*</tt> to grant access to all subcommands of the given command",
)
regexMatch = QuerySetRegexMatch.as_manager()

grant_type = models.CharField(max_length=32, choices=AccessGrantTypeChoices)

Expand Down
59 changes: 59 additions & 0 deletions nautobot_chatops/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,29 @@ def setup_db(self):
name="user3",
value="3333",
)

AccessGrant.objects.create(
command="x",
subcommand="get-.*",
grant_type=AccessGrantTypeChoices.TYPE_ORGANIZATION,
name="org3",
value="33",
)
AccessGrant.objects.create(
command="x",
subcommand="get-.*",
grant_type=AccessGrantTypeChoices.TYPE_CHANNEL,
name="channel3",
value="333",
)
AccessGrant.objects.create(
command="x",
subcommand="get-.*",
grant_type=AccessGrantTypeChoices.TYPE_USER,
name="user3",
value="3333",
)

# And some wildcard access grants:
AccessGrant.objects.create(
command="y",
Expand Down Expand Up @@ -313,6 +336,42 @@ def test_permitted_subcommand(self, mock_enqueue_task):
)
mock_enqueue_task.assert_not_called()

def test_permitted_subcommand_re(self, mock_enqueue_task):
"""A per-subcommand access grant applies that subcommands under that command, and no others."""
self.setup_db()
for cmd, subcmd in [("x", "get-device"), ("x", "get-prefix-status"), ("x", "")]:
mock_enqueue_task.reset_mock()
check_and_enqueue_command(
self.mock_registry,
cmd,
subcmd,
[],
{"org_id": "33", "channel_id": "333", "user_id": "3333"},
MockDispatcher.reset(),
)
self.assertIsNone(MockDispatcher.error)
mock_enqueue_task.assert_called_once()

def test_not_permitted_re(self, mock_enqueue_task):
"""Per-user access grants are checked."""
self.setup_db()
for cmd, subcmd in [("x", "set-device")]:
mock_enqueue_task.reset_mock()
check_and_enqueue_command(
self.mock_registry,
cmd,
subcmd,
[],
{"org_id": "33", "channel_id": "333", "user_id": "3333"},
MockDispatcher.reset(),
)
self.assertEqual(
MockDispatcher.error,
"Access to this bot and/or command is not permitted in organization 33, "
"ask your Nautobot administrator to define an appropriate Access Grant",
)
mock_enqueue_task.assert_not_called()

def test_not_permitted_user(self, mock_enqueue_task):
"""Per-user access grants are checked."""
self.setup_db()
Expand Down
5 changes: 4 additions & 1 deletion nautobot_chatops/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,10 @@ def check_and_enqueue_command(
else:
# Actual subcommand - permit only if this particular subcommand (or all commands/subcommands) are permitted
access_grants = AccessGrant.objects.filter(
Q(command="*") | Q(command=command, subcommand="*") | Q(command=command, subcommand=subcommand),
Q(command="*")
| Q(command=command, subcommand="*")
| Q(command=command, subcommand=subcommand)
| Q(command=command, subcommand__in=AccessGrant.regexMatch.match_re(subcommand))
)

org_grants = access_grants.filter(grant_type=AccessGrantTypeChoices.TYPE_ORGANIZATION)
Expand Down