-
Notifications
You must be signed in to change notification settings - Fork 2
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
add initial message endpoint #31
Conversation
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.
@brngylni Please squash the commits and every other changes required on the codebase can be done after the merge.
@@ -0,0 +1,63 @@ | |||
|
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.
Please remove the empty line from the top of the file.
|
||
|
||
|
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.
Please leave two lines of difference between each segregated block definition.
|
||
headers = get_github_headers(request.headers) | ||
|
||
secret = "00b863c62c96aa67aeea966104362b1136ce883d" # will come from a data source |
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.
Please read this from a configuration file rather than hardcoding it into the code itself.
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.
Will change here after the database models are changed
def verify_signature(payload_body: str, secret_token: str, signature_header: str) -> bool: | ||
"""Verify that the payload was sent from GitHub by validating SHA256. | ||
|
||
Raise and return 403 if not authorized. | ||
|
||
Args: | ||
payload_body: original request body to verify (request.body()) | ||
secret_token: GitHub app webhook token (WEBHOOK_SECRET) | ||
signature_header: header received from GitHub (x-hub-signature-256) | ||
""" | ||
if not signature_header: | ||
return False | ||
hash_object = hmac.new(secret_token.encode('utf-8'), msg=payload_body, digestmod=hashlib.sha256) | ||
expected_signature = "sha256=" + hash_object.hexdigest() | ||
|
||
if not hmac.compare_digest(expected_signature, signature_header): | ||
return False | ||
return True |
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.
Is it intentional to have the identical code block for the function definition of verify_signature
in the parser
module? If not, please remove it from either of the locations.
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.
I'll remove it from util file
|
||
|
||
def fas_by_github(username: str) -> str: | ||
"""Get the Fedora Account System Username of the given github username |
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.
GitHub is a proper noun.
"""Get the Fedora Account System Username of the given github username | |
"""Get the Fedora Account System Username of the given GitHub username |
"""Get the Fedora Account System Username of the given github username | ||
|
||
Args: | ||
username: Github Username""" |
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.
GitHub is a proper noun.
username: Github Username""" | |
username: GitHub Username""" |
@@ -9,7 +9,7 @@ class Defaults: | |||
PERMANENT_SESSION_LIFETIME = 604800 | |||
SESSION_COOKIE_NAME = "user" | |||
|
|||
|
|||
FASJSON_URL = "https://fasjson.tinystage.test/fasjson" |
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.
The default should be something reasonable for production use, such as https://fasjson.fedoraproject.org
.
The development value should be put in the development configuration file: devel/ansible/roles/dev/files/config.toml
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.
You still need to set the development value in devel/ansible/roles/dev/files/config.toml
|
||
|
||
app = Flask(__name__) | ||
message_endpoint = Blueprint("message_endpoint", __name__) |
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.
Blueprints are supposed to contain more than one view. You can define a generic blueprint for the app in endpoints.__init__.py
and register the views with it.
Examples: https://github.com/fedora-infra/noggin/blob/dev/noggin/controller/__init__.py and https://github.com/fedora-infra/noggin/blob/dev/noggin/controller/root.py
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.
This may conflict with other branches I guess. We might need another pr to switch this to the one you supposed. Since the other branches are merged.
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.
I'm not sure I understand. Which branches will it conflict with?
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.
I have the code you suggested on local but after a meeting with @gridhead we thought the current one might be better. So whichever you guys decide I can push.
Used for creating a new message by sending a post request to /message path | ||
|
||
Request Body: | ||
service_uuid: Service related to message. |
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.
We can't expect the message body to always contain a key named service_uuid
, because we don't control the content of the webhook. Instead, we can put the UUID in the URL route. You can change the route pattern in the decorator to: /message/<service_uuid>
, and add service_uuid
to the function's arguments.
service_uuid: Service related to message. | ||
|
||
""" | ||
if not validate_request(request, fields=['service_uuid']): |
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.
You don't need to pass request
, it's a global object.
|
||
|
||
def not_found() -> Response: | ||
return Response({'message': 'Not Found'}, status=404, mimetype='application/json') |
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.
This is equivalent to doing return {'message', 'Not Found'}, 404
:
- if you return a dict from a view, Flask will automatically convert it to JSON and set the Content-Type header
- if you return a tuple from a view with a number as the second element, Flask will use it as the return code.
Please look at this Flask doc about error handling in API servers
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.
I'm going to fix it here https://github.com/fedora-infra/webhook-to-fedora-messaging/pull/32/files
|
||
|
||
def success(data: dict) ->Response: | ||
return Response(data, status=200, mimetype='application/json') |
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.
This is equivalent to just returning data
, no need for this function.
|
||
|
||
def bad_request() -> Response: | ||
return Response("{'message': 'Bad Request'}", status=400, mimetype='application/json') |
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.
This is equivalent to returning {'message': 'Bad Request'}
, 400. Please see the doc above to setup a JSON-aware error handler that will allow us to just raise exceptions or use flask.abort()
.
|
||
|
||
def created(data: dict) -> Response: | ||
return Response(data, status=201, mimetype='application/json') |
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.
This is equivalent to returning data, 201
, no need for this function
|
||
|
||
def conflict(data: dict) -> Response: | ||
return Response(data, status=409, mimetype='application/json') |
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.
Same thing here.
858fe10
to
259b420
Compare
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.
I am not dragging this any longer than it has to be. Other required changes can be made to the codebase after it is merged.
Merging now.
Please rebase this branch, I'll review it afterwards. |
259b420
to
18b8888
Compare
Rebased it |
try: | ||
service = db.session.scalar(select(Service).where(Service.uuid == service_uuid)).one() | ||
except NoResultFound: | ||
return {'message': 'Not Found'}, 404 |
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.
You can provide a better message, such as Service UUID not found
.
|
||
if msg is not None: | ||
api.publish(msg) | ||
return {'message': 'Success'}, 200 |
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.
You don't need to add , 200
, it's the default.
Also, you can maybe provide a better response, such as:
return {'message': 'Success'}, 200 | |
return {'status': 'OK', 'message_id': msg.id} |
This will help debugging in case something goes wrong: we'll be able to search for the message by ID in datanommer.
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.
I'm not sure if messages have an id attribute in our case. Since we don't log the messages, I thought we don't have that
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.
Yeah this message object is a Fedora Messaging Message instance, so it does have an ID. It's not a database object.
api.publish(msg) | ||
return {'message': 'Success'}, 200 | ||
else: | ||
return abort(400, {'message': "Bad Request"}) |
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.
You can provide a better message here, for example The webhook could not be converted to a message
(if that's what's going on).
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.
I think we can remove the else block since we already use a try-except block above
if service_type.lower() == "github": | ||
return _github_parser(secret) | ||
else: | ||
raise ValueError |
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.
Please use an instance with a message:
raise ValueError | |
raise ValueError(f"Unsupported service: {service_type}") |
def _github_parser(secret: str) -> GithubMessageV1: | ||
"""Convert Flask request objects into desired FedMsg format. | ||
|
||
Args: |
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.
This args list does not match the actual arguments.
except SignatureMatchError: | ||
return abort(400, {'message': "Bad Request"}) |
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.
You can also provide a better message here, just reuse the message associated with the exception:
except SignatureMatchError: | |
return abort(400, {'message': "Bad Request"}) | |
except SignatureMatchError as e: | |
return abort(400, {'message': str(e)}) |
Return false if not authorized. | ||
|
||
Args: | ||
payload_body: original request body to verify (request.body()) |
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.
This arg is not there anymore.
if not hmac.compare_digest(expected_signature, signature_header): | ||
return False | ||
return True |
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.
You can just do:
if not hmac.compare_digest(expected_signature, signature_header): | |
return False | |
return True | |
return hmac.compare_digest(expected_signature, signature_header) |
@@ -0,0 +1,2 @@ | |||
class SignatureMatchError(Exception): |
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.
There is already an exceptions.py
file in the main directory, please use it.
raise ValueError | ||
|
||
|
||
def _github_parser(secret: str) -> GithubMessageV1: |
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.
Please put this Github-specific code into a github.py
module (also put the verify_signature()
function there as it is Github-specific as well).
568fc73
to
3cda15b
Compare
Signed-off-by: Mehmet Baran Geylani <[email protected]>
5ccdf8f
to
742e8d8
Compare
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.
Looks good, thanks!
This PR adds the initial message endpoint. #5