Skip to content
This repository has been archived by the owner on Nov 7, 2019. It is now read-only.

Commit

Permalink
Merge pull request #1 from josecoelho96/dev/requests-origin-check
Browse files Browse the repository at this point in the history
Add request origin validation
  • Loading branch information
josecoelho96 authored Sep 21, 2018
2 parents c787de8 + b07230e commit 621f6e7
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 95 deletions.
1 change: 1 addition & 0 deletions .env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ APP_PORT_EXTERNAL=

# Slack related
SLACK_SUPPORT_CHANNEL_ID=
SLACK_SIGNING_SECRET=
61 changes: 23 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,28 @@
# neecathon-slack-bot
A slack bot for the NEECathon!

## Valid commands:
```./criar-equipa [team name]``` - Creates a new team, if the name doesn't exists already.\
Returns the team ID, a key to later join the team and the team name.


```./entrar [team-code]``` - Joins the team with the defined code, if exists.\
TODO: Join the user to channels.


```./saldo``` - Shows the current balance (team-wise).


```./compra [@user] [qtd] [description]``` - Allows to buy something from another user.
TODO: Posts a message to channels.


```./movimentos <qtd>``` - List transactions. \
This command can be used by either users and admins. \
If the user has a team, list the last `qtd` transactions of his team. \
**TODO:** \
If performed by an admin, list the last `qtd` transactions of all teams. \
An admin can add the `team-id` as a argument to view the transactions of only one team.

## Valid commands
### Create team
`/criar-equipa [team name]`
Creates a new team, if the name doesn't exists already. Returns the newly created team information: The name, ID and a access key, which allows users to enter the team using that code. Reports an error stating that a team cannot be created if something fails. If the team name already exists the team isn't created and an error message appears in the chat.
### Join team
`/entrar [entry-code]`
Joins the team with the defined `entry-code`, if exists. If the `entry-code` is valid, the user receives a message and joins the team. If it's invalid, an error message pops up.
### Balance check
`/saldo`
Shows the team-wise current balance. If the user does not have a team, an error message appears stating how to join a team.
### Buy
`/compra [@destination_user] [qty] [description]`
Allows to buy something from another user. It performs a transfer, between the command caller and the `destination_user`, by giving him `qty` credits. A short description must be provided to describe the transaction. If `destination_user` isn't enrolled in a team, an error message will be displayed stating that. If `qty` is invalid (unparsable, negative, null or above team actual balance), the user will get an error message explaining the problem.
### List last transactions
`/movimentos <qty>`
List transactions. If the user has a team, list the last `qty` transactions of his team. If the current user doesn't have a team, an error message appears stating how to join a team.


## Current features:
- Request origin verification/validation

## To be added



```./ver-equipas``` - List all teams. \
Can only be performed by admins. Used to list all teams.

Expand All @@ -41,30 +35,21 @@ Can only be performed by admins. Used to list all details of a team. The `team-i
Can only be performed by admins. Used to change all teams balances.


```./adicionar-informacao <nome|email> <value>``` \
Can be performed by users, to add some personal information on his account (name and email). \
An admin can also use this command on other user, by providing the `@user` has a first argument.

```./informacoes``` \
If performed by an user/admin, returns all informations related to his account. \
An admin can also use this command to retrieve all information on some user by providing the `@user`.


```./tornar-admin <@user>``` \
Can only be performed by admins. Used to make `@user` an admin.




## Features to add
- Verify requests origin and validation
- Auto add users to channels
- Report logs to channel
- Report money receival on buy operation
- Permissions system
- Error codes

## Problems found
- How to create first admin.
- Implementation: Log levels aren't well defined.
- IDs are not being verified as unique.

## Bug list
- ...
4 changes: 3 additions & 1 deletion src/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@
"LIST_TRANSACTIONS": "/movimentos",
}

INITIAL_TEAM_BALANCE = 200
INITIAL_TEAM_BALANCE = 200

SLACK_REQUEST_TIMESTAMP_MAX_GAP_MINUTES = 1.0
194 changes: 138 additions & 56 deletions src/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
import dispatcher
from threading import Thread
from bottle import request
from definitions import SLACK_REQUEST_DATA_KEYS
from definitions import SLACK_REQUEST_DATA_KEYS, SLACK_REQUEST_TIMESTAMP_MAX_GAP_MINUTES
import common
import time
import database
import exceptions
import hmac
import hashlib
import os


common.setup_logger()

Expand All @@ -13,98 +20,173 @@ def create_team():
log.debug("New create team request.")
request_data = dict(request.POST)

if all_elements_on_request(request_data):
# Procceed with request.
log.debug("Request with correct fields, add to queue.")
if dispatcher.add_request_to_queue(request_data):
# Request was added to queue
return responder.confirm_create_team_command_reception()
if check_request_origin(request):
if all_elements_on_request(request_data):
# Procceed with request.
log.debug("Request with correct fields, add to queue.")
if dispatcher.add_request_to_queue(request_data):
# Request was added to queue
return responder.confirm_create_team_command_reception()
else:
# Request wasn't added to queue
return responder.overloaded_error()
else:
# Request wasn't added to queue
return responder.overloaded_error()
# Inform user of incomplete request.
log.warn("Request with invalid payload was sent.")
return responder.default_error()
else:
# Inform user of incomplete request.
log.warn("Request with invalid payload was sent.")
return responder.default_error()
# Could not validate user request
log.error("Slack request origin verification failed.")
try:
database.save_request_log(request_data, False, "Unverified origin.")
except exceptions.SaveRequestLogError:
log.error("Failed to save request log.")
return responder.unverified_origin_error()

def join_team():
"""Handler to join team request."""
log.debug("New join team request.")
request_data = dict(request.POST)

if all_elements_on_request(request_data):
# Procceed with request.
log.debug("Request with correct fields, add to queue.")
if dispatcher.add_request_to_queue(request_data):
# Request was added to queue
return responder.confirm_join_team_command_reception()
if check_request_origin(request):
if all_elements_on_request(request_data):
# Procceed with request.
log.debug("Request with correct fields, add to queue.")
if dispatcher.add_request_to_queue(request_data):
# Request was added to queue
return responder.confirm_join_team_command_reception()
else:
# Request wasn't added to queue
return responder.overloaded_error()
else:
# Request wasn't added to queue
return responder.overloaded_error()
# Inform user of incomplete request.
log.warn("Request with invalid payload was sent.")
return responder.default_error()
else:
# Inform user of incomplete request.
log.warn("Request with invalid payload was sent.")
return responder.default_error()
# Could not validate user request
log.error("Slack request origin verification failed.")
try:
database.save_request_log(request_data, False, "Unverified origin.")
except exceptions.SaveRequestLogError:
log.error("Failed to save request log.")
return responder.unverified_origin_error()

def check_balance():
"""Handler to check balance request."""
log.debug("New check balance request.")
request_data = dict(request.POST)

if all_elements_on_request(request_data):
# Procceed with request.
log.debug("Request with correct fields, add to queue.")
if dispatcher.add_request_to_queue(request_data):
# Request was added to queue
return responder.confirm_check_balance_command_reception()
if check_request_origin(request):
if all_elements_on_request(request_data):
# Procceed with request.
log.debug("Request with correct fields, add to queue.")
if dispatcher.add_request_to_queue(request_data):
# Request was added to queue
return responder.confirm_check_balance_command_reception()
else:
# Request wasn't added to queue
return responder.overloaded_error()
else:
# Request wasn't added to queue
return responder.overloaded_error()
# Inform user of incomplete request.
log.warn("Request with invalid payload was sent.")
return responder.default_error()
else:
# Inform user of incomplete request.
log.warn("Request with invalid payload was sent.")
return responder.default_error()
# Could not validate user request
log.error("Slack request origin verification failed.")
try:
database.save_request_log(request_data, False, "Unverified origin.")
except exceptions.SaveRequestLogError:
log.error("Failed to save request log.")
return responder.unverified_origin_error()

def buy():
"""Handler to buy request."""
log.debug("New buy request.")
request_data = dict(request.POST)

if all_elements_on_request(request_data):
# Procceed with request.
log.debug("Request with correct fields, add to queue.")
if dispatcher.add_request_to_queue(request_data):
# Request was added to queue
return responder.confirm_buy_command_reception()
if check_request_origin(request):
if all_elements_on_request(request_data):
# Procceed with request.
log.debug("Request with correct fields, add to queue.")
if dispatcher.add_request_to_queue(request_data):
# Request was added to queue
return responder.confirm_buy_command_reception()
else:
# Request wasn't added to queue
return responder.overloaded_error()
else:
# Request wasn't added to queue
return responder.overloaded_error()
# Inform user of incomplete request.
log.warn("Request with invalid payload was sent.")
return responder.default_error()
else:
# Inform user of incomplete request.
log.warn("Request with invalid payload was sent.")
return responder.default_error()
# Could not validate user request
log.error("Slack request origin verification failed.")
try:
database.save_request_log(request_data, False, "Unverified origin.")
except exceptions.SaveRequestLogError:
log.error("Failed to save request log.")
return responder.unverified_origin_error()

def list_transactions():
"""Handler to list transactions request."""
log.debug("New list transactions request.")
request_data = dict(request.POST)

if all_elements_on_request(request_data):
# Procceed with request.
log.debug("Request with correct fields, add to queue.")
if dispatcher.add_request_to_queue(request_data):
# Request was added to queue
return responder.confirm_list_transactions_command_reception()
if check_request_origin(request):
if all_elements_on_request(request_data):
# Procceed with request.
log.debug("Request with correct fields, add to queue.")
if dispatcher.add_request_to_queue(request_data):
# Request was added to queue
return responder.confirm_list_transactions_command_reception()
else:
# Request wasn't added to queue
return responder.overloaded_error()
else:
# Request wasn't added to queue
return responder.overloaded_error()
# Inform user of incomplete request.
log.warn("Request with invalid payload was sent.")
return responder.default_error()
else:
# Inform user of incomplete request.
log.warn("Request with invalid payload was sent.")
return responder.default_error()
# Could not validate user request
log.error("Slack request origin verification failed.")
try:
database.save_request_log(request_data, False, "Unverified origin.")
except exceptions.SaveRequestLogError:
log.error("Failed to save request log.")
return responder.unverified_origin_error()

def all_elements_on_request(request_data):
"""Check if all elements (keys) are present in the request dictionary"""
if all(k in request_data for k in SLACK_REQUEST_DATA_KEYS):
return True
return False

def check_request_origin(request):
"""Check if a request origin matches Slack definitions."""
request_timestamp = request.get_header("X-Slack-Request-Timestamp")
if request_timestamp:
slack_signature = request.get_header("X-Slack-Signature")
if slack_signature:
if abs(float(request_timestamp) - time.time()) < SLACK_REQUEST_TIMESTAMP_MAX_GAP_MINUTES:
# Request within gap
request_body = request.body.read().decode("utf-8")
signing_secret = bytes(os.getenv("SLACK_SIGNING_SECRET"), "utf-8")
base_string = "v0:{}:{}".format(request_timestamp, request_body).encode("utf-8")
computed_signature = "v0=" + hmac.new(signing_secret, msg = base_string, digestmod=hashlib.sha256).hexdigest()

if hmac.compare_digest(slack_signature, computed_signature):
log.debug("Request origin verified.")
return True
else:
log.critical("'X-Slack-Signature' header value and computed signature don't match.")
return False
else:
log.critical("Header 'X-Slack-Request-Timestamp' value is different than handler server. Refusing request.")
return False
else:
# No header
log.critical("Header 'X-Slack-Signature' not present. Refusing request.")
return False
else:
log.critical("Header 'X-Slack-Request-Timestamp' not present. Refusing request.")
return False
9 changes: 9 additions & 0 deletions src/responder.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,15 @@ def overloaded_error():
}
return json.dumps(response_content, ensure_ascii=False).encode("utf-8")

def unverified_origin_error():
"""Immediate default response to an overloaded error."""
response.add_header("Content-Type", "application/json")
response_content = {
"text": "O teu pedido tem origens suspeitas...\nO teu pedido não pode ser processado.\nTenta novamente mais tarde ou pede ajuda no <#{}|suporte>."
.format(get_support_channel_id()),
}
return json.dumps(response_content, ensure_ascii=False).encode("utf-8")

def get_support_channel_id():
"""Get slack support channel id."""
return os.getenv("SLACK_SUPPORT_CHANNEL_ID")
Expand Down

0 comments on commit 621f6e7

Please sign in to comment.