Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User permissions #258

Merged
merged 20 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 10 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
1 change: 1 addition & 0 deletions changes/112.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ability to link Chat User with Nautobot User.
10 changes: 10 additions & 0 deletions docs/admin/install/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This guide outlines the process of enabling Nautobot ChatOps, which includes:
- [Installation Guide](#installation-guide)
- [Configuration Guide](#configuration-guide)
- [Granting Access to the Chat Platform](#granting-access-to-the-chat-platform)
- [Link Nautobot Account](#link-nautobot-account)
- [Test Your Chatbot](#test-your-chatbot)
- [Integrations Configuration](#integrations-configuration)

Expand Down Expand Up @@ -115,6 +116,7 @@ Adjust the App's behavior with the following settings:
| `delete_input_on_submission` | Removes the input prompt from the chat history after user input | No | `False` |
| `restrict_help` | Shows Help prompt only to users based on their Access Grants | No | `False` |
| `send_all_messages_private` | Ensures only the person interacting with the bot sees the responses | No | `False` |
| `fallback_chatops_user` | Nautobot User for Chat Commands to use if the user has not linked their account | Yes | `chatbot` |
| `session_cache_timeout` | Controls session cache | No | `86400` |

## Granting Access to the Chat Platform
Expand All @@ -125,6 +127,14 @@ Adjust the App's behavior with the following settings:
heading-offset=1
%}

## Link Nautobot Account

{%
include-markdown '../../models/chatopsaccountlink.md'
start='<!--account-link-->'
heading-offset=1
%}

## Test Your Chatbot

Finally, test your chatbot's functionality within your chosen chat application, using a command like `/nautobot get-devices`.
Expand Down
1 change: 1 addition & 0 deletions docs/admin/install/slack_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ While this method is still possible, we recommend using the App Manifest method
- `files:write`
- `incoming-webhook`
- `users:read`
- `users:read.email`
- `app_mentions:read`
- `groups:read`
- `im:read`
Expand Down
Binary file added docs/images/account_link.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions docs/models/chatopsaccountlink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Account Link

<!--account-link-->
+++3.0.0

Nautobot ChatOps now uses the built-in Nautobot permissions for Nautobot Objects (Devices, Locations, Racks, etc.). Each user will need to link their Nautobot Account with their Chat Platform User Account. Login to Nautobot then access the Link ChatOps Account within the Plugins menu. Here you can provide your email address and select the ChatOps Platform you are using, then click the Look up User ID from Email to get your Chat User ID.

A Nautobot user can have multiple Chat users connected, but the Chat User can only be linked to one Nautobot user. As an example my team is transitioning from Slack to Mattermost. My Slack User ID and my Mattermost User ID can both be connected to the same Nautobot User.

![Link Accounts](../images/account_link.png)

## Configuring Account Link for many users

Admins have the ability to access the Nautobot Admin page and can use `ChatOps Account Links` page to link multiple accounts, but you will need to know the Chat Platforms user ids for each user you are linking.
smk4664 marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 5 additions & 1 deletion docs/user/app_faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ If a 404 error is being returned while trying to use a slash command that allows

Slack does not currently support using slash commands within a conversation thread. Nautobot can be mentioned in a thread and will parse the text after the bot's name for a command.

For example, if you want to run the slash command `/nautobot get-devices site site-a`, the equivalent bot mention command would be (assuming your bot name is `@nautobot`) `@nautobot nautobot get-devices site site-a`.
For example, if you want to run the slash command `/nautobot get-devices site site-a`, the equivalent bot mention command would be (assuming your bot name is `@nautobot`) `@nautobot nautobot get-devices site site-a`.

## The Chat Commands do not allow me to select locations/devices/interfaces/etc

Nautobot ChatOps now uses your Nautobot user permissions in order to process commands. Please link your Chat Account with your Nautobot Account.
8 changes: 8 additions & 0 deletions docs/user/app_getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ A step-by-step tutorial on how to get the App going and how to use it.

To install the App, please follow the instructions detailed in the [Administrator Guide](../admin/install/index.md).

## Link Nautobot Account

+++3.0.0

Nautobot ChatOps now uses the built-in Nautobot permissions for Nautobot Objects (Devices, Locations, Racks, etc.). Each user will need to link their Nautobot Account with their Chat Platform User Account. Login to Nautobot then access the Link ChatOps Account within the Plugins menu. Here you can provide your email address and select the ChatOps Platform you are using, then click the Look up User ID from Email to get your Chat User ID.

![Link Accounts](../images/account_link.png)

## Built-in Commands

Each command can be invoked with `help` sub-command to display all sub-commands with the description.
Expand Down
1 change: 1 addition & 0 deletions nautobot_chatops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class NautobotChatOpsConfig(PluginConfig):
# Should menus, text input fields, etc. be deleted from the chat history after the user makes a selection?
"delete_input_on_submission": False,
"restrict_help": False,
"fallback_chatops_user": "chatbot",
smk4664 marked this conversation as resolved.
Show resolved Hide resolved
# As requested on https://github.com/nautobot/nautobot-plugin-chatops/issues/114 this setting is used for
# sending all messages as an ephemeral message, meaning only the person interacting with the bot will see the
# responses.
Expand Down
15 changes: 14 additions & 1 deletion nautobot_chatops/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Administrative capabilities for nautobot_chatops plugin."""

from django.contrib import admin
from .models import CommandLog
from .models import CommandLog, ChatOpsAccountLink


@admin.register(CommandLog)
Expand All @@ -17,4 +17,17 @@ class CommandLogAdmin(admin.ModelAdmin):
"platform",
"command",
"subcommand",
"system_user",
)


@admin.register(ChatOpsAccountLink)
class ChatOpsAccountLinkAdmin(admin.ModelAdmin):
"""Administrative view for managing ChatOps Account Link instances."""

list_display = (
"pk",
"nautobot_user",
"platform",
"user_id",
)
7 changes: 5 additions & 2 deletions nautobot_chatops/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@
CommandTokenViewSet,
NautobotChatopsRootView,
)
from nautobot_chatops.api.views.lookup import AccessLookupView
from nautobot_chatops.api.views.lookup import AccessLookupView, UserEmailLookupView

_APP_CONFIG: Dict = settings.PLUGINS_CONFIG["nautobot_chatops"]

logger = logging.getLogger(__name__)
urlpatterns = [path("lookup/", AccessLookupView.as_view(), name="access_lookup")]
urlpatterns = [
path("lookup/", AccessLookupView.as_view(), name="access_lookup"),
path("email-lookup/", UserEmailLookupView.as_view(), name="email_lookup"),
]

if _APP_CONFIG.get("enable_slack"):
from nautobot_chatops.api.views.slack import SlackSlashCommandView, SlackInteractionView, SlackEventAPIView
Expand Down
26 changes: 25 additions & 1 deletion nautobot_chatops/api/views/lookup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""API views for dynamic lookup of platform-specific data."""

import contextlib
from django.http import JsonResponse, HttpResponseBadRequest, HttpResponseNotFound
from django.views import View

Expand All @@ -12,7 +13,7 @@ class AccessLookupView(View):
http_method_names = ["get"]

def get(self, request, *args, **kwargs):
"""Handle an inbount GET request for a specific access grant value."""
"""Handle an inbound GET request for a specific access grant value."""
for required_param in ("grant_type", "name"):
if required_param not in request.GET:
return HttpResponseBadRequest(f"Missing mandatory parameter {required_param}")
Expand All @@ -32,3 +33,26 @@ def get(self, request, *args, **kwargs):
return HttpResponseNotFound(f"No {request.GET['grant_type']} {request.GET['name']} found")

return JsonResponse(data={"value": value})


class UserEmailLookupView(View):
"""Look up a user_id by email."""

http_method_names = ["get"]

def get(self, request, *args, **kwargs):
"""Handle an inbound GET request for a specific access grant value."""
for required_param in ("email", "platform"):
if required_param not in request.GET:
return HttpResponseBadRequest(f"Missing mandatory parameter {required_param}")
smk4664 marked this conversation as resolved.
Show resolved Hide resolved

value = None
for dispatcher_class in Dispatcher.subclasses():
if dispatcher_class.platform_slug == request.GET["platform"]:
with contextlib.suppress(NotImplementedError):
value = dispatcher_class.lookup_user_id_by_email(request.GET["email"])
return (
JsonResponse(data={"user_id": value})
if value
else HttpResponseNotFound(f"No user_id found for {request.GET['email']}")
)
smk4664 marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions nautobot_chatops/api/views/ms_teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def post(self, request, *args, **kwargs):
"user_id": body["from"]["id"],
"user_name": body["from"]["name"],
"user_role": body["from"].get("role"),
"user_ad_id": body["from"].get("aadObjectId"),
"conversation_id": body["conversation"]["id"],
"conversation_name": body["conversation"].get("name"),
"bot_id": body["recipient"]["id"],
Expand Down
28 changes: 28 additions & 0 deletions nautobot_chatops/banner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Banner to alert Staff to use Admin site if trying to add/edit Account Links for other Users."""
from typing import Optional

from django.urls import reverse
from django.utils.html import format_html

from nautobot.extras.choices import BannerClassChoices
from nautobot.extras.plugins import PluginBanner


def banner(context, *args, **kwargs) -> Optional[PluginBanner]:
"""
Construct the ChatOps Account Link Banner.

This Banner will provide link to alert Staff users of the
admin site list/add/edit options for managing other users
ChatOps Account Links.
"""
if "chatops/account-link/" in context.request.path and (
context.request.user.is_staff or context.request.user.is_superuser
):
content = format_html(
"This page is to manage your ChatOps Account Links, to manage other users ChatOps Account Links,"
'visit the <a href="{}">Admin Site</a>',
reverse("admin:nautobot_chatops_chatopsaccountlink_changelist"),
)
return PluginBanner(content=content, banner_class=BannerClassChoices.CLASS_INFO)
return None
16 changes: 16 additions & 0 deletions nautobot_chatops/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,19 @@ class CommandTokenPlatformChoices(ChoiceSet):
WEBEX = "webex"

CHOICES = ((MATTERMOST, "Mattermost"),)


class PlatformChoices(ChoiceSet):
smk4664 marked this conversation as resolved.
Show resolved Hide resolved
"""Choices for the Platform field."""

MATTERMOST = "mattermost"
SLACK = "slack"
MS_TEAMS = "microsoft_teams"
WEBEX = "webex"

CHOICES = (
(MATTERMOST, "Mattermost"),
(MS_TEAMS, "Microsoft Teams"),
(SLACK, "Slack"),
(WEBEX, "Webex Teams"),
)
1 change: 1 addition & 0 deletions nautobot_chatops/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
COMMAND_TOKEN_TOKEN_HELP_TEXT = (
"Token given by chat platform for signing or command validation" # nosec - skips Bandit B105 error
)
CHATOPS_USER_ID_HELP_TEXT = "Enter the chat platform's User ID you want to link."
35 changes: 35 additions & 0 deletions nautobot_chatops/dispatchers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
import logging
from typing import Dict, Optional
from django.templatetags.static import static
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings

from nautobot_chatops.models import ChatOpsAccountLink
from texttable import Texttable

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -38,6 +41,26 @@ def __init__(self, context=None):
"""Init this Dispatcher with the provided dict of contextual information (which will vary by app)."""
self.context = context or {}

@property
def user(self):
"""Dispatcher property containing the Nautobot User that is linked to the Chat User."""

if self.context.get("user_id"):
try:
return ChatOpsAccountLink.objects.get(
platform=self.platform_slug, user_id=self.context["user_id"]
).nautobot_user
except ObjectDoesNotExist:
logger.warning(
f"Could not find User matching {self.context['user_name']} - id: {self.context['user_id']}."
"Add a ChatOps User to link the accounts."
)
user_model = get_user_model()
user, _ = user_model.objects.get_or_create(
username=settings.PLUGINS_CONFIG["nautobot_chatops"]["fallback_chatops_user"]
)
smk4664 marked this conversation as resolved.
Show resolved Hide resolved
return user

def _get_cache_key(self) -> str:
"""Key generator for the cache, adding the plugin prefix name."""
# Using __file__ as a key customization within the cache
Expand Down Expand Up @@ -127,6 +150,18 @@ def platform_lookup(cls, item_type, item_name) -> Optional[str]:
"""
raise NotImplementedError

@classmethod
def lookup_user_id_by_email(cls, email) -> Optional[str]:
"""Call out to the chat platform to look up a specific user ID by email.

Args:
email (str): Uniquely identifying email address of the user.

Returns:
(str, None)
"""
raise NotImplementedError

def static_url(self, path):
"""Construct an absolute URL for the given static file path, such as "nautobot/NautobotLogoSquare.png"."""
static_path = str(static(path))
Expand Down
17 changes: 17 additions & 0 deletions nautobot_chatops/dispatchers/mattermost.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,23 @@ def platform_lookup(cls, item_type, item_name) -> Optional[str]:

return None

@classmethod
def lookup_user_id_by_email(cls, email) -> Optional[str]:
"""Call out to Mattermost to look up a specific user ID by email.

Args:
email (str): Uniquely identifying email address of the user.

Returns:
(str, None)
"""
instance = cls(context=None)
try:
response = instance.mm_client.get(f"/users/email/{email}")
return response["id"]
except NotFoundException:
return None

# More complex APIs for presenting structured data - these typically build on the more basic functions below
def command_response_header(self, command, subcommand, args, description="information", image_element=None):
"""Construct a consistently forwarded header including the command that was issued.
Expand Down
Loading
Loading