From 5c0ed674758bc262bed3de4934c20e7e0b1605f9 Mon Sep 17 00:00:00 2001 From: jsudano Date: Sat, 2 May 2020 14:22:44 -0700 Subject: [PATCH] Added __init__ parameter to bot such that users can supply a function which the bot can use to approve users, similar to the \`approved_users\` list added documentation --- README.md | 80 +++++++++++++++++++++++----------- tests/test_webexteamsbot.py | 46 +++++++++++++++++++ webexteamsbot/webexteamsbot.py | 32 +++++++++++--- 3 files changed, 127 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index ceb540a..3122a84 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # webexteamsbot -This package makes creating [Webex Teams](https://www.webex.com/products/teams/index.html) bots in Python super simple. +This package makes creating [Webex Teams](https://www.webex.com/products/teams/index.html) bots in Python super simple. [![PyPI version](https://badge.fury.io/py/webexteamsbot.svg)](https://badge.fury.io/py/webexteamsbot) [![published](https://static.production.devnetcloud.com/codeexchange/assets/images/devnet-published.svg)](https://developer.cisco.com/codeexchange/github/repo/hpreston/webexteamsbot) -> This package is based on the previous [ciscosparkbot](https://github.com/imapex/ciscosparkbot) project. This version will both move to new Webex Teams branding as well as add new functionality. If you've used `ciscosparkbot` you will find this package very similar and familiar. +> This package is based on the previous [ciscosparkbot](https://github.com/imapex/ciscosparkbot) project. This version will both move to new Webex Teams branding as well as add new functionality. If you've used `ciscosparkbot` you will find this package very similar and familiar. # Prerequisites @@ -34,7 +34,7 @@ If you don't already have a [Webex Teams](https://www.webex.com/products/teams/i # Installation -> Python 3.6+ is recommended. Python 2.7 should also work. +> Python 3.6+ is recommended. Python 2.7 should also work. 1. Create a virtualenv and install the module @@ -57,7 +57,7 @@ If you don't already have a [Webex Teams](https://www.webex.com/products/teams/i export TEAMS_BOT_APP_NAME= ``` -1. A basic bot requires very little code to get going. +1. A basic bot requires very little code to get going. ```python import os @@ -97,7 +97,7 @@ If you don't already have a [Webex Teams](https://www.webex.com/products/teams/i bot.run(host="0.0.0.0", port=5000) ``` -1. A [sample script](https://github.com/hpreston/webexteamsbot/blob/master/sample.py) that shows more advanced bot features and customization is also provided in the repo. +1. A [sample script](https://github.com/hpreston/webexteamsbot/blob/master/sample.py) that shows more advanced bot features and customization is also provided in the repo. ## Advanced Options ### Changing the Help Message @@ -121,13 +121,13 @@ If you don't already have a [Webex Teams](https://www.webex.com/products/teams/i You also need a way to catch anything other than "messages", which is the only thing handled entirely inside the bot framework. Continuing the example of monitoring for membership changes in a room, you would also need to add a "command" to catch the membership events. You would use the following to do so: ```python - # check membership:all webhook to verify that person added to room (or otherwise modified) is allowed to be in the room + # check membership:all webhook to verify that person added to room (or otherwise modified) is allowed to be in the room def check_memberships(api, incoming_msg): wl_dom = os.getenv("WHITELIST_DOMAINS") if wl_dom.find("[") < 0: wl_dom = '["' + wl_dom + '"]' wl_dom = wl_dom.replace(",", '","') - + if wl_dom and incoming_msg["event"] != "deleted": pemail = incoming_msg["data"]["personEmail"] pid = incoming_msg["data"]["personId"] @@ -145,11 +145,11 @@ If you don't already have a [Webex Teams](https://www.webex.com/products/teams/i "it is restricted to employees.") return "'" + pemail + "' was automatically removed from this space; it is restricted to only " \ "internal users." - + return "" - - ###### - + + ###### + bot.add_command('memberships', '*', check_memberships) ``` The first argument, "memberships", tells the bot to look for resources of the type "memberships", the second argument "*" instructs the bot that this is not something that should be included in the internal "help" command, and the third command is the function to execute to handle the membership creation. @@ -174,25 +174,25 @@ If you don't already have a [Webex Teams](https://www.webex.com/products/teams/i 'content-type': 'application/json; charset=utf-8', 'authorization': 'Bearer ' + teams_token } - + url = 'https://api.ciscospark.com/v1/attachment/actions/' + attachmentid response = requests.get(url, headers=headers) return response.json() - # check attachmentActions:created webhook to handle any card actions + # check attachmentActions:created webhook to handle any card actions def handle_cards(api, incoming_msg): m = get_attachment_actions(incoming_msg["data"]["id"]) print(m) - + return m["inputs"] - - ###### - + + ###### + bot.add_command('attachmentActions', '*', handle_cards) ``` The first argument, "attachmentActions", tells the bot to look for resources of the type "attachmentActions", the second argument "*" instructs the bot that this is not something that should be included in the internal "help" command, and the third command is the function to execute to handle the card action. -### Creating arbitrary HTTP Endpoints/URLs +### Creating arbitrary HTTP Endpoints/URLs 1. You can also add a new path to Flask by using the "add_new_url" command. You can use this so that the bot can handle things other than Webex Teams Webhooks. For example, if you wanted to receive other webhooks to the "/webhooks" path, you would use this: ```python def handle_webhooks(): @@ -203,15 +203,17 @@ If you don't already have a [Webex Teams](https://www.webex.com/products/teams/i netid = webhook_event["networkId"] print(netid) - ###### + ###### bot.add_new_url("/webhooks", "webhooks", handle_webhooks) ``` The first argument, "/webhooks", represents the URL path to listen for, the second argument represents the Flask endpoint, and the third command is the function to execute to handle GET, PUT, or POST actions. -### Limiting Who Can Interact with the Bot -1. By default the bot will reply to any Webex Teams user who talks with it. But you may want to setup a Bot that only talks to "approved users". -1. Start by creating a list of email addresses of your approved users. +### Limiting Who Can Interact with the Bot +1. By default the bot will reply to any Webex Teams user who talks with it. But you may want to setup a Bot that only talks to "approved users". + +##### By list +1. Start by creating a list of email addresses of your approved users. ```python approved_users = [ @@ -219,7 +221,7 @@ If you don't already have a [Webex Teams](https://www.webex.com/products/teams/i ] ``` -1. Now when creating the bot object, simply add the `approved_users` parameter. +1. Now when creating the bot object, simply add the `approved_users` parameter. ```python bot = TeamsBot( @@ -231,13 +233,39 @@ If you don't already have a [Webex Teams](https://www.webex.com/products/teams/i ) ``` -1. Now if a users **NOT** listed in the `approved_users` list attempts to communicate with the bot, the message will be ignored and a notification is logged. +1. Now if a users **NOT** listed in the `approved_users` list attempts to communicate with the bot, the message will be ignored and a notification is logged. ``` Message from: hapresto@cisco.com - User: hapresto@cisco.com is not approved to interact with bot. Ignoring. + User: hapresto@cisco.com is not in the list of approved users. Ignoring. + ``` + +##### By function +1. Start by writing a function which takes an email string and returns a bool + ```python + def approve_user(email): + return email.endswith("@cisco.com") ``` +1. Now when creating the bot object, simply add the `user_approval_function` parameter. + ```python + bot = TeamsBot( + bot_app_name, + teams_bot_token=teams_token, + teams_bot_url=bot_url, + teams_bot_email=bot_email, + user_approval_function=approve_user + ) + ``` + +1. Now if a user is **NOT** approved by the `user_approval_function` and they attempt to use the bot, the message will be ignored and a notification is logged. + ``` + Message from: bob@gmail.com + User: bob@gmail.com is not approved by function. Ignoring. + ``` + +1. *Note:* if both list and function methods are used, only users which are in the `approved_users` list **AND** satisfy the `user_approval_function` will be allowed to interact. + # ngrok [ngrok](http://ngrok.com) will make easy for you to develop your code with a live bot. @@ -318,7 +346,7 @@ coverage html This will generate a code coverage report in a directory called `htmlcov` # Credits -The initial packaging of the original `ciscosparkbot` project was done by [Kevin Corbin](https://github.com/kecorbin). +The initial packaging of the original `ciscosparkbot` project was done by [Kevin Corbin](https://github.com/kecorbin). This package was created with [Cookiecutter](https://github.com/audreyr/cookiecutter) and the diff --git a/tests/test_webexteamsbot.py b/tests/test_webexteamsbot.py index 4e77a42..e2676f6 100644 --- a/tests/test_webexteamsbot.py +++ b/tests/test_webexteamsbot.py @@ -285,5 +285,51 @@ def test_process_incoming_message_from_bot(self, m): self.assertEqual(resp.status_code, 200) print(resp.data) + @requests_mock.mock() + def test_user_approval(self, m): + print(self.app) + m.get( + "https://api.ciscospark.com/v1/webhooks", + json=MockTeamsAPI.list_webhooks(), + ) + m.post( + "https://api.ciscospark.com/v1/webhooks", + json=MockTeamsAPI.create_webhook(), + ) + bot_email = "test@test.com" + teams_token = "somefaketoken" + bot_url = "http://fakebot.com" + bot_app_name = "testbot" + # Create a new bot + bot = TeamsBot(bot_app_name, + teams_bot_token=teams_token, + teams_bot_url=bot_url, + teams_bot_email=bot_email, + debug=True) + + approval_f = bot._user_approved + + # default approve all users + self.assertTrue(approval_f('test@domain1.com')) + self.assertTrue(approval_f('test@domain2.com')) + + # approve only by list + bot.approved_users = ['test@domain1.com'] + self.assertTrue(approval_f('test@domain1.com')) + self.assertFalse(approval_f('test@domain2.com')) + + # approve only by function + bot.approved_users = [] + bot.user_approval_function = lambda x: x.endswith('@domain1.com') + self.assertTrue(approval_f('test@domain1.com')) + self.assertFalse(approval_f('test@domain2.com')) + + # approve by both function and list (both must be satisfied) + bot.approved_users = ['test@domain2.com', 'test@domain1.com'] + self.assertTrue(approval_f('test@domain1.com')) # approved by both + self.assertFalse(approval_f('test2@domain1.com')) # not approved by list + self.assertFalse(approval_f('test@domain2.com')) # not approved by function + self.assertFalse(approval_f('test@domain3.com')) # not approved by both + def tearDown(self): pass diff --git a/webexteamsbot/webexteamsbot.py b/webexteamsbot/webexteamsbot.py index 611c97d..c2ad990 100644 --- a/webexteamsbot/webexteamsbot.py +++ b/webexteamsbot/webexteamsbot.py @@ -28,7 +28,8 @@ def __init__( webhook_resource_event=None, webhook_resource="messages", webhook_event="created", - approved_users=[], + approved_users=[], + user_approval_function=None, debug=False, ): """ @@ -49,7 +50,10 @@ def __init__( to create webhooks for. [{"resource": "messages", "event": "created"}, {"resource": "attachmentActions", "event": "created"}] - :param approved_users: List of approved users (by email) to interact with bot. Default all users. + :param approved_users: List of approved users (by email) to interact + with bot. Default all users. + :param user_approval_function: boolean function to approve users (by + email). Default all users. :param debug: boolean value for debut messages """ @@ -74,6 +78,7 @@ def __init__( self.teams_bot_url = teams_bot_url self.default_action = default_action self.approved_users = approved_users + self.user_approval_function = user_approval_function self.webhook_resource = webhook_resource self.webhook_event = webhook_event self.webhook_resource_event = webhook_resource_event @@ -269,6 +274,25 @@ def health(self): """ return "I'm Alive" + def _user_approved(self, user_email): + """ + Determines whether user_email is allowed by given approval parameters + :return: bool + """ + if len(self.approved_users) > 0 and \ + user_email not in self.approved_users: + # User NOT approved + sys.stderr.write("User: " + user_email + + " is not in list of approved users. Ignoring.\n") + return False + if self.user_approval_function and \ + not self.user_approval_function(user_email): + # User NOT approved + sys.stderr.write("User: " + user_email + + " is not approved by function. Ignoring.\n") + return False + return True + def process_incoming_message(self): """ Process an incoming message, determine the command and action, @@ -313,9 +337,7 @@ def process_incoming_message(self): sys.stderr.write("Message from: " + message.personEmail + "\n") # Check if user is approved - if len(self.approved_users) > 0 and message.personEmail not in self.approved_users: - # User NOT approved - sys.stderr.write("User: " + message.personEmail + " is not approved to interact with bot. Ignoring.\n") + if not self._user_approved(message.personEmail): return "Unapproved user" # Find the command that was sent, if any