Implemented Blender ID webhook for user change notifications
Handles changes in: - roles - full name - email address
This commit is contained in:
@@ -41,6 +41,7 @@ class CloudExtension(PillarExtension):
|
||||
return {
|
||||
'EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER': 'https://store.blender.org/api/',
|
||||
'EXTERNAL_SUBSCRIPTIONS_TIMEOUT_SECS': 10,
|
||||
'BLENDER_ID_WEBHOOK_USER_CHANGED_SECRET': 'oos9wah1Zoa0Yau6ahThohleiChephoi',
|
||||
}
|
||||
|
||||
def eve_settings(self):
|
||||
@@ -93,10 +94,11 @@ class CloudExtension(PillarExtension):
|
||||
"""
|
||||
|
||||
from pillar.api.service import signal_user_changed_role
|
||||
from . import routes
|
||||
from . import routes, webhooks
|
||||
|
||||
signal_user_changed_role.connect(self._user_changed_role)
|
||||
routes.setup_app(app)
|
||||
app.register_api_blueprint(webhooks.blueprint, '/webhooks')
|
||||
|
||||
def _user_changed_role(self, sender, user: dict):
|
||||
from pillar.api import service
|
||||
|
97
cloud/webhooks.py
Normal file
97
cloud/webhooks.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""Blender ID webhooks."""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
|
||||
from flask_login import request
|
||||
from flask import Blueprint
|
||||
import werkzeug.exceptions as wz_exceptions
|
||||
|
||||
from pillar import current_app
|
||||
from pillar.api.blender_cloud import subscription
|
||||
from pillar.auth import UserClass
|
||||
|
||||
blueprint = Blueprint('cloud-webhooks', __name__)
|
||||
log = logging.getLogger(__name__)
|
||||
WEBHOOK_MAX_BODY_SIZE = 1024 * 10 # 10 kB is large enough for
|
||||
|
||||
|
||||
def webhook_payload(hmac_secret: str) -> dict:
|
||||
"""Obtains the webhook payload from the request, verifying its HMAC.
|
||||
|
||||
:returns the webhook payload as dictionary.
|
||||
"""
|
||||
# Check the content type
|
||||
if request.content_type != 'application/json':
|
||||
log.info('request from %s to %s had bad content type %s',
|
||||
request.remote_addr, request, request.content_type)
|
||||
raise wz_exceptions.BadRequest('Content type not supported')
|
||||
|
||||
# Check the length of the body
|
||||
if request.content_length > WEBHOOK_MAX_BODY_SIZE:
|
||||
raise wz_exceptions.BadRequest('Request too large')
|
||||
body = request.get_data()
|
||||
if len(body) > request.content_length:
|
||||
raise wz_exceptions.BadRequest('Larger body than Content-Length header')
|
||||
|
||||
# Validate the request
|
||||
mac = hmac.new(hmac_secret.encode(), body, hashlib.sha256)
|
||||
req_hmac = request.headers.get('X-Webhook-HMAC', '')
|
||||
our_hmac = mac.hexdigest()
|
||||
if not hmac.compare_digest(req_hmac, our_hmac):
|
||||
log.info('request from %s to %s had bad HMAC %r, expected %r',
|
||||
request.remote_addr, request, req_hmac, our_hmac)
|
||||
raise wz_exceptions.BadRequest('Bad HMAC')
|
||||
|
||||
try:
|
||||
return json.loads(body)
|
||||
except json.JSONDecodeError as ex:
|
||||
log.warning('request from %s to %s had bad JSON: %s',
|
||||
request.remote_addr, request, ex)
|
||||
raise wz_exceptions.BadRequest('Bad JSON')
|
||||
|
||||
|
||||
@blueprint.route('/user-modified', methods=['POST'])
|
||||
def user_modified():
|
||||
"""Updates the local user based on the info from Blender ID."""
|
||||
my_log = log.getChild('user_modified')
|
||||
my_log.debug('Received request from %s', request.remote_addr)
|
||||
|
||||
hmac_secret = current_app.config['BLENDER_ID_WEBHOOK_USER_CHANGED_SECRET']
|
||||
payload = webhook_payload(hmac_secret)
|
||||
|
||||
my_log.info('payload: %s', payload)
|
||||
|
||||
# Update the user
|
||||
users_coll = current_app.db('users')
|
||||
db_user = users_coll.find_one({'email': payload['old_email']})
|
||||
if not db_user:
|
||||
my_log.warning('Received update for unknown user %r', payload['old_email'])
|
||||
return '', 204
|
||||
|
||||
# Use direct database updates to change the email and full name.
|
||||
updates = {}
|
||||
if payload['old_email'] != payload['email']:
|
||||
my_log.info('User changed email from %s to %s', payload['old_email'], payload['email'])
|
||||
updates['email'] = payload['email']
|
||||
|
||||
if payload['full_name'] != db_user['full_name']:
|
||||
my_log.info('User changed full name from %s to %s',
|
||||
payload['full_name'], db_user['full_name'])
|
||||
updates['full_name'] = payload['full_name']
|
||||
|
||||
if updates:
|
||||
update_res = users_coll.update_one({'_id': db_user['_id']},
|
||||
{'$set': updates})
|
||||
if update_res.matched_count != 1:
|
||||
my_log.error('Unable to find user %s to update, even though '
|
||||
'we found them by email address %s',
|
||||
db_user['_id'], payload['old_email'])
|
||||
|
||||
# Defer to Pillar to do the role updates.
|
||||
local_user = UserClass.construct('', db_user)
|
||||
subscription.do_update_subscription(local_user, payload)
|
||||
|
||||
return '', 204
|
168
tests/test_webhooks.py
Normal file
168
tests/test_webhooks.py
Normal file
@@ -0,0 +1,168 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
from abstract_cloud_test import AbstractCloudTest
|
||||
|
||||
|
||||
class UserModifiedTest(AbstractCloudTest):
|
||||
def setUp(self, **kwargs):
|
||||
super().setUp(**kwargs)
|
||||
self.enter_app_context()
|
||||
self.hmac_secret = b'1234 je moeder'
|
||||
self.app.config['BLENDER_ID_WEBHOOK_USER_CHANGED_SECRET'] = self.hmac_secret.decode()
|
||||
self.uid = self.create_user(24 * 'a',
|
||||
roles={'subscriber'},
|
||||
email='old@email.address')
|
||||
|
||||
def test_change_full_name(self):
|
||||
payload = {'id': 1112333,
|
||||
'old_email': 'old@email.address',
|
||||
'full_name': 'ကြယ်ဆွတ်',
|
||||
'email': 'old@email.address',
|
||||
'roles': ['cloud_subscriber']}
|
||||
as_json = json.dumps(payload).encode()
|
||||
mac = hmac.new(self.hmac_secret,
|
||||
as_json, hashlib.sha256)
|
||||
self.post('/api/webhooks/user-modified',
|
||||
data=as_json,
|
||||
content_type='application/json',
|
||||
headers={'X-Webhook-HMAC': mac.hexdigest()},
|
||||
expected_status=204)
|
||||
|
||||
# Check the effect on the user
|
||||
db_user = self.fetch_user_from_db(self.uid)
|
||||
self.assertEqual('old@email.address', db_user['email'])
|
||||
self.assertEqual('ကြယ်ဆွတ်', db_user['full_name'])
|
||||
self.assertEqual(['subscriber'], db_user['roles'])
|
||||
|
||||
def test_change_email(self):
|
||||
payload = {'id': 1112333,
|
||||
'old_email': 'old@email.address',
|
||||
'full_name': 'ကြယ်ဆွတ်',
|
||||
'email': 'new.address+here-there@email.address',
|
||||
'roles': ['cloud_subscriber']}
|
||||
as_json = json.dumps(payload).encode()
|
||||
mac = hmac.new(self.hmac_secret,
|
||||
as_json, hashlib.sha256)
|
||||
self.post('/api/webhooks/user-modified',
|
||||
data=as_json,
|
||||
content_type='application/json',
|
||||
headers={'X-Webhook-HMAC': mac.hexdigest()},
|
||||
expected_status=204)
|
||||
|
||||
# Check the effect on the user
|
||||
db_user = self.fetch_user_from_db(self.uid)
|
||||
self.assertEqual('new.address+here-there@email.address', db_user['email'])
|
||||
self.assertEqual('ကြယ်ဆွတ်', db_user['full_name'])
|
||||
self.assertEqual(['subscriber'], db_user['roles'])
|
||||
|
||||
def test_change_roles(self):
|
||||
payload = {'id': 1112333,
|
||||
'old_email': 'old@email.address',
|
||||
'full_name': 'ကြယ်ဆွတ်',
|
||||
'email': 'old@email.address',
|
||||
'roles': ['cloud_demo']}
|
||||
as_json = json.dumps(payload).encode()
|
||||
mac = hmac.new(self.hmac_secret,
|
||||
as_json, hashlib.sha256)
|
||||
self.post('/api/webhooks/user-modified',
|
||||
data=as_json,
|
||||
content_type='application/json',
|
||||
headers={'X-Webhook-HMAC': mac.hexdigest()},
|
||||
expected_status=204)
|
||||
|
||||
# Check the effect on the user
|
||||
db_user = self.fetch_user_from_db(self.uid)
|
||||
self.assertEqual('old@email.address', db_user['email'])
|
||||
self.assertEqual('ကြယ်ဆွတ်', db_user['full_name'])
|
||||
self.assertEqual({'flamenco-user', 'attract-user', 'demo'}, set(db_user['roles']))
|
||||
|
||||
def test_bad_hmac(self):
|
||||
payload = {'id': 1112333,
|
||||
'old_email': 'old@email.address',
|
||||
'full_name': 'ကြယ်ဆွတ်',
|
||||
'email': 'new@email.address',
|
||||
'roles': ['cloud_demo']}
|
||||
as_json = json.dumps(payload).encode()
|
||||
mac = hmac.new(self.hmac_secret,
|
||||
as_json, hashlib.sha256)
|
||||
self.post('/api/webhooks/user-modified',
|
||||
data=as_json,
|
||||
content_type='application/json',
|
||||
headers={'X-Webhook-HMAC': mac.hexdigest()[:-2]},
|
||||
expected_status=400)
|
||||
|
||||
# Check the effect on the user
|
||||
db_user = self.fetch_user_from_db(self.uid)
|
||||
self.assertEqual('old@email.address', db_user['email'])
|
||||
self.assertEqual('คนรักของผัดไทย', db_user['full_name'])
|
||||
self.assertEqual({'subscriber'}, set(db_user['roles']))
|
||||
|
||||
def test_no_hmac(self):
|
||||
payload = {'id': 1112333,
|
||||
'old_email': 'old@email.address',
|
||||
'full_name': 'ကြယ်ဆွတ်',
|
||||
'email': 'new@email.address',
|
||||
'roles': ['cloud_demo']}
|
||||
as_json = json.dumps(payload).encode()
|
||||
self.post('/api/webhooks/user-modified',
|
||||
data=as_json,
|
||||
content_type='application/json',
|
||||
expected_status=400)
|
||||
|
||||
# Check the effect on the user
|
||||
db_user = self.fetch_user_from_db(self.uid)
|
||||
self.assertEqual('old@email.address', db_user['email'])
|
||||
self.assertEqual('คนรักของผัดไทย', db_user['full_name'])
|
||||
self.assertEqual({'subscriber'}, set(db_user['roles']))
|
||||
|
||||
def test_unknown_email(self):
|
||||
payload = {'id': 1112333,
|
||||
'old_email': 'unknown@email.address',
|
||||
'full_name': 'ကြယ်ဆွတ်',
|
||||
'email': 'new@email.address',
|
||||
'roles': ['cloud_demo']}
|
||||
as_json = json.dumps(payload).encode()
|
||||
mac = hmac.new(self.hmac_secret,
|
||||
as_json, hashlib.sha256)
|
||||
self.post('/api/webhooks/user-modified',
|
||||
data=as_json,
|
||||
content_type='application/json',
|
||||
headers={'X-Webhook-HMAC': mac.hexdigest()},
|
||||
expected_status=204)
|
||||
|
||||
# Check the effect on the user
|
||||
db_user = self.fetch_user_from_db(self.uid)
|
||||
self.assertEqual('old@email.address', db_user['email'])
|
||||
self.assertEqual('คนรักของผัดไทย', db_user['full_name'])
|
||||
self.assertEqual({'subscriber'}, set(db_user['roles']))
|
||||
|
||||
def test_huge_request(self):
|
||||
payload = b'a' * 1024 * 100
|
||||
mac = hmac.new(self.hmac_secret, payload, hashlib.sha256)
|
||||
self.post('/api/webhooks/user-modified',
|
||||
data=payload,
|
||||
content_type='application/json',
|
||||
headers={'X-Webhook-HMAC': mac.hexdigest()},
|
||||
expected_status=400)
|
||||
|
||||
# Check the effect on the user
|
||||
db_user = self.fetch_user_from_db(self.uid)
|
||||
self.assertEqual('old@email.address', db_user['email'])
|
||||
self.assertEqual('คนรักของผัดไทย', db_user['full_name'])
|
||||
self.assertEqual({'subscriber'}, set(db_user['roles']))
|
||||
|
||||
def test_invalid_json(self):
|
||||
payload = b'\x00' * 1024 * 5
|
||||
mac = hmac.new(self.hmac_secret, payload, hashlib.sha256)
|
||||
self.post('/api/webhooks/user-modified',
|
||||
data=payload,
|
||||
content_type='application/json',
|
||||
headers={'X-Webhook-HMAC': mac.hexdigest()},
|
||||
expected_status=400)
|
||||
|
||||
# Check the effect on the user
|
||||
db_user = self.fetch_user_from_db(self.uid)
|
||||
self.assertEqual('old@email.address', db_user['email'])
|
||||
self.assertEqual('คนรักของผัดไทย', db_user['full_name'])
|
||||
self.assertEqual({'subscriber'}, set(db_user['roles']))
|
Reference in New Issue
Block a user