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

add initial message endpoint #31

Merged
merged 1 commit into from
Jul 24, 2024

Conversation

brngylni
Copy link
Contributor

@brngylni brngylni commented Jul 9, 2024

This PR adds the initial message endpoint. #5

Copy link
Member

@gridhead gridhead left a 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 @@

Copy link
Member

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.

Comment on lines 8 to 10



Copy link
Member

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
Copy link
Member

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.

Copy link
Contributor Author

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

Comment on lines 30 to 47
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
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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.

Suggested change
"""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"""
Copy link
Member

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.

Suggested change
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"
Copy link
Member

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

Copy link
Member

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

webhook_to_fedora_messaging/endpoints/message.py Outdated Show resolved Hide resolved


app = Flask(__name__)
message_endpoint = Blueprint("message_endpoint", __name__)
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

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.
Copy link
Member

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']):
Copy link
Member

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')
Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.



def success(data: dict) ->Response:
return Response(data, status=200, mimetype='application/json')
Copy link
Member

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')
Copy link
Member

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')
Copy link
Member

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')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here.

@brngylni brngylni force-pushed the dev_message_endpoint branch 2 times, most recently from 858fe10 to 259b420 Compare July 16, 2024 06:35
Copy link
Member

@gridhead gridhead left a 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.

@abompard
Copy link
Member

Please rebase this branch, I'll review it afterwards.

@brngylni
Copy link
Contributor Author

Please rebase this branch, I'll review it afterwards.

Rebased it

@brngylni brngylni requested a review from abompard July 23, 2024 11:47
try:
service = db.session.scalar(select(Service).where(Service.uuid == service_uuid)).one()
except NoResultFound:
return {'message': 'Not Found'}, 404
Copy link
Member

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
Copy link
Member

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:

Suggested change
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.

Copy link
Contributor Author

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

Copy link
Member

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"})
Copy link
Member

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).

Copy link
Contributor Author

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
Copy link
Member

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:

Suggested change
raise ValueError
raise ValueError(f"Unsupported service: {service_type}")

def _github_parser(secret: str) -> GithubMessageV1:
"""Convert Flask request objects into desired FedMsg format.

Args:
Copy link
Member

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.

Comment on lines 33 to 34
except SignatureMatchError:
return abort(400, {'message': "Bad Request"})
Copy link
Member

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:

Suggested change
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())
Copy link
Member

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.

Comment on lines 51 to 53
if not hmac.compare_digest(expected_signature, signature_header):
return False
return True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just do:

Suggested change
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):
Copy link
Member

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:
Copy link
Member

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).

@brngylni brngylni force-pushed the dev_message_endpoint branch 3 times, most recently from 568fc73 to 3cda15b Compare July 24, 2024 07:38
Signed-off-by: Mehmet Baran Geylani <[email protected]>
Copy link
Member

@abompard abompard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks!

@abompard abompard merged commit c5d101f into fedora-infra:main Jul 24, 2024
5 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants