-
Notifications
You must be signed in to change notification settings - Fork 0
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
Webhook #29
Closed
+1,147
−334
Closed
Webhook #29
Changes from 59 commits
Commits
Show all changes
79 commits
Select commit
Hold shift + click to select a range
702e076
Format using Blue, drop unused imports
kaapstorm 065ce5f
Fix unimported exception
kaapstorm 79a30af
isort, drop unused imports
kaapstorm c23f53a
Define undefined exception, drop unused comment
kaapstorm bfb8c7f
Move refresh_hq_datasource() out of views.py
kaapstorm 04cc01b
Create webhook endpoint
kaapstorm 10f9436
Happy path up front to improve readability
kaapstorm a449215
Move `convert_to_array()` to utils
kaapstorm ced4d8f
Nit: Typo
kaapstorm e3c6d65
Pull out function
kaapstorm bcc6d71
Define `update_dataset()`
kaapstorm 4ab6617
Move superset imports locally
kaapstorm 7e1c379
Call `add_view()`
kaapstorm e9e2207
Add details to README
kaapstorm 591857f
Error handling in DataSetChangeAPI view
kaapstorm db84723
Define `update_dataset()`
kaapstorm b6f9ec4
Add `blinker` as a dependency
kaapstorm 6507f64
Add oauth models and authorization api
Charl1996 26bdaa0
Add scope check and better error handling
Charl1996 612616f
Update error message
Charl1996 3281a2b
Pull hq requests out into class
Charl1996 08b91f6
Add subscriber task and move models to separate file
Charl1996 405c266
Add require_oauth decorator
Charl1996 70b280f
Fix accessor of response
Charl1996 fdaedb7
Minor changes
Charl1996 934ca9c
Add scope back to Token and add grant types on client
Charl1996 b0480b3
Undo my hack
Charl1996 19df6d0
Invoke celery task to subscribe to datasource
Charl1996 b746cc6
Fix circular import
Charl1996 7fdfff1
Add BASE_URL to config
Charl1996 e69828f
Some fixes
Charl1996 0bbbee6
Append test_download_datasource
Charl1996 7f5ec64
Add require_oauth decorator to endpoint
Charl1996 4279f4c
Remove breakpoint
Charl1996 3338ffe
Uncomment presumably useful line
Charl1996 b6dd2a4
models.py: Support Python 3.8
kaapstorm a838483
`setup_hq_db()` assumes test DB is Postgres
kaapstorm 2250eb7
Add comments to clarify methods
Charl1996 5e919ca
Make use of timedelta days instead of seconds
Charl1996 c6f6bac
Use more secure method for generating a client_secret
Charl1996 43cd6b6
Increase length of client_secret
Charl1996 8524beb
Merge pull request #30 from dimagi/cs/SC-3069-subscribe-to-ds-changes
kaapstorm 950077e
Revert "Add `blinker` as a dependency"
kaapstorm f0a820a
Move DataSetChangeAPI to api module
kaapstorm faa8269
--wip-- Troubleshooting: Upgrade Superset to master
kaapstorm 1a3679d
--wip-- Config, and test nitpicks
kaapstorm 76ce919
Merge branch 'master' into nh/webhook
kaapstorm 8a9e263
Fix tests
kaapstorm fa34441
Merge branch 'master' into nh/webhook
kaapstorm e4cdb41
Document rookie mistake
kaapstorm 571c3bb
Better error message
kaapstorm 9765272
"CommCare HQ" has a space
kaapstorm afb1ee6
Give an error message if unable to connect to HQ
kaapstorm bc06e03
Set HQ Data URI in `superset_config.py`
kaapstorm f287af4
isort
kaapstorm 1154a80
Use SQLAlchemy bind for HQ Data db
kaapstorm 73e32d4
Add migration
kaapstorm ef72e21
Add how to create a migration to docs
kaapstorm 54f5d31
Call `subscribe_to_hq_datasource()` synchronously
kaapstorm 00c244c
`abort()` requires a code
kaapstorm b4502e0
Dependency inversion: Create services layer
kaapstorm 76ac389
Dependency inversion: Move util to model method
kaapstorm 801b962
Move URL functions into a module
kaapstorm 4aea365
Use symmetric encryption for client secrets
kaapstorm f8b13eb
Don't hardcode URLs
kaapstorm b59dd0f
Move OAuth2 server code to its own module
kaapstorm d81eba0
A few nits
kaapstorm c2a43e8
Simplify check_client_secret()
kaapstorm 1a52c35
Add `AUTHLIB_INSECURE_TRANSPORT` to README.md
kaapstorm 1d6c6e4
Avoid Celery pickle error
kaapstorm 1f7dc4b
Drop `get_explore_database()`
kaapstorm 133b7ee
Fix DataSetChange
kaapstorm b2aa3cb
Add API endpoints to domain-excluded views
kaapstorm e66e002
Simplify update_dataset()
kaapstorm 83538b7
Cast data to column types
kaapstorm a11b348
Small refactor `update_dataset()` fixes insert
kaapstorm b375cb5
Don't bother with non-pk index column
kaapstorm e7d921e
Fix decorator
kaapstorm 8a91695
Token extends OAuth2TokenMixin
kaapstorm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import json | ||
from datetime import datetime, timedelta | ||
from http import HTTPStatus | ||
|
||
from authlib.integrations.flask_oauth2 import ( | ||
AuthorizationServer, | ||
ResourceProtector, | ||
) | ||
from authlib.oauth2.rfc6749 import grants | ||
from authlib.oauth2.rfc6750 import BearerTokenValidator | ||
from flask import jsonify, request | ||
from flask_appbuilder.api import BaseApi, expose | ||
from flask_appbuilder.baseviews import expose_api | ||
from sqlalchemy.orm.exc import NoResultFound | ||
from superset import db | ||
from superset.extensions import appbuilder, csrf | ||
from superset.superset_typing import FlaskResponse | ||
from superset.views.base import handle_api_exception, json_error_response | ||
|
||
from .models import DataSetChange, HQClient, Token | ||
from .utils import update_dataset | ||
|
||
require_oauth = ResourceProtector() | ||
app = appbuilder.app | ||
|
||
|
||
def query_client(client_id): | ||
return HQClient.get_by_client_id(client_id) | ||
|
||
|
||
def save_token(token, request): | ||
client = request.client | ||
client.revoke_tokens() | ||
|
||
expires_at = datetime.utcnow() + timedelta(days=1) | ||
tok = Token( | ||
client_id=client.client_id, | ||
expires_at=expires_at, | ||
access_token=token['access_token'], | ||
token_type=token['token_type'], | ||
scope=client.domain, | ||
) | ||
db.session.add(tok) | ||
db.session.commit() | ||
|
||
|
||
class HQBearerTokenValidator(BearerTokenValidator): | ||
def authenticate_token(self, token_string): | ||
return db.session.query(Token).filter_by(access_token=token_string).first() | ||
|
||
|
||
require_oauth.register_token_validator(HQBearerTokenValidator()) | ||
|
||
authorization = AuthorizationServer( | ||
app=app, | ||
query_client=query_client, | ||
save_token=save_token, | ||
) | ||
authorization.register_grant(grants.ClientCredentialsGrant) | ||
|
||
|
||
class OAuth(BaseApi): | ||
|
||
def __init__(self): | ||
super().__init__() | ||
self.route_base = "/oauth" | ||
|
||
@expose("/token", methods=('POST',)) | ||
def issue_access_token(self): | ||
try: | ||
response = authorization.create_token_response() | ||
except NoResultFound: | ||
return jsonify({"error": "Invalid client"}), 401 | ||
|
||
if response.status_code >= 400: | ||
return response | ||
|
||
data = json.loads(response.data.decode("utf-8")) | ||
return jsonify(data) | ||
|
||
|
||
class DataSetChangeAPI(BaseApi): | ||
""" | ||
Accepts changes to datasets from CommCare HQ data forwarding | ||
""" | ||
|
||
MAX_REQUEST_LENGTH = 10_485_760 # reject >10MB JSON requests | ||
|
||
def __init__(self): | ||
self.route_base = '/hq_webhook' | ||
self.default_view = 'post_dataset_change' | ||
super().__init__() | ||
|
||
# http://localhost:8088/hq_webhook/change/ | ||
@expose_api(url='/change/', methods=('POST',)) | ||
@handle_api_exception | ||
@csrf.exempt | ||
@require_oauth | ||
def post_dataset_change(self) -> FlaskResponse: | ||
if request.content_length > self.MAX_REQUEST_LENGTH: | ||
return json_error_response( | ||
HTTPStatus.REQUEST_ENTITY_TOO_LARGE.description, | ||
status=HTTPStatus.REQUEST_ENTITY_TOO_LARGE.value, | ||
) | ||
|
||
try: | ||
request_json = json.loads(request.get_data(as_text=True)) | ||
change = DataSetChange(**request_json) | ||
update_dataset(change) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitty: Thoughts on moving |
||
return self.json_response( | ||
'Request accepted; updating dataset', | ||
status=HTTPStatus.ACCEPTED.value, | ||
) | ||
except json.JSONDecodeError: | ||
return json_error_response( | ||
'Invalid JSON syntax', | ||
status=HTTPStatus.BAD_REQUEST.value, | ||
) | ||
except (TypeError, ValueError) as err: | ||
return json_error_response( | ||
str(err), | ||
status=HTTPStatus.BAD_REQUEST.value, | ||
) | ||
# `@handle_api_exception` will return other exceptions as JSON | ||
# with status code 500, e.g. | ||
# {"error": "CommCare HQ database missing"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# The name of the database for storing data related to CommCare HQ | ||
HQ_DATA = "HQ Data" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import superset | ||
from hq_superset.oauth import get_valid_cchq_oauth_token | ||
|
||
|
||
class HqUrl: | ||
@classmethod | ||
def datasource_export_url(cls, domain, datasource_id): | ||
return f"a/{domain}/configurable_reports/data_sources/export/{datasource_id}/?format=csv" | ||
|
||
@classmethod | ||
def datasource_list_url(cls, domain): | ||
return f"a/{domain}/api/v0.5/ucr_data_source/" | ||
|
||
@classmethod | ||
def datasource_details_url(cls, domain, datasource_id): | ||
return f"a/{domain}/api/v0.5/ucr_data_source/{datasource_id}/" | ||
|
||
@classmethod | ||
def subscribe_to_datasource_url(cls, domain, datasource_id): | ||
return f"a/{domain}/configurable_reports/data_sources/subscribe/{datasource_id}/" | ||
|
||
|
||
class HQRequest: | ||
|
||
def __init__(self, url): | ||
self.url = url | ||
|
||
@property | ||
def oauth_token(self): | ||
return get_valid_cchq_oauth_token() | ||
|
||
@property | ||
def commcare_provider(self): | ||
return superset.appbuilder.sm.oauth_remotes["commcare"] | ||
|
||
@property | ||
def api_base_url(self): | ||
return self.commcare_provider.api_base_url | ||
|
||
@property | ||
def absolute_url(self): | ||
return f"{self.api_base_url}{self.url}" | ||
|
||
def get(self): | ||
return self.commcare_provider.get(self.url, token=self.oauth_token) | ||
|
||
def post(self, data): | ||
return self.commcare_provider.post(self.url, data=data, token=self.oauth_token) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Generic single-database configuration. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🥳