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

Added init parameter for a user-configured function to approve users #6

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
80 changes: 54 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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=<your bots 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
Expand Down Expand Up @@ -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
Expand All @@ -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"]
Expand All @@ -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.
Expand All @@ -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():
Expand All @@ -203,23 +203,25 @@ 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 = [
"[email protected]",
]
```

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(
Expand All @@ -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: [email protected]
User: [email protected] is not approved to interact with bot. Ignoring.
User: [email protected] 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: [email protected]
User: [email protected] 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.
Expand Down Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions tests/test_webexteamsbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]"
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('[email protected]'))
self.assertTrue(approval_f('[email protected]'))

# approve only by list
bot.approved_users = ['[email protected]']
self.assertTrue(approval_f('[email protected]'))
self.assertFalse(approval_f('[email protected]'))

# approve only by function
bot.approved_users = []
bot.user_approval_function = lambda x: x.endswith('@domain1.com')
self.assertTrue(approval_f('[email protected]'))
self.assertFalse(approval_f('[email protected]'))

# approve by both function and list (both must be satisfied)
bot.approved_users = ['[email protected]', '[email protected]']
self.assertTrue(approval_f('[email protected]')) # approved by both
self.assertFalse(approval_f('[email protected]')) # not approved by list
self.assertFalse(approval_f('[email protected]')) # not approved by function
self.assertFalse(approval_f('[email protected]')) # not approved by both

def tearDown(self):
pass
32 changes: 27 additions & 5 deletions webexteamsbot/webexteamsbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
):
"""
Expand All @@ -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
"""

Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down