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

rest: add share_workflow endpoint #538

Closed
wants to merge 2 commits into from

Conversation

DaanRosendal
Copy link
Member

Adds a new endpoint to share a workflow with a user.

Closes reanahub/reana-client#680

Copy link
Member

@mdonadoni mdonadoni left a comment

Choose a reason for hiding this comment

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

Overall looks good! Also very happy to see many tests ;)

This PR also needs to be rebased

Comment on lines +41 to +44
from sqlalchemy import and_, nullslast, or_
from sqlalchemy.orm import aliased
from webargs import fields
from webargs.flaskparser import use_args, use_kwargs
Copy link
Member

Choose a reason for hiding this comment

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

These imports should be placed before importing REANA packages, see https://github.com/reanahub/reana/wiki/Tips-for-Python#imports

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

The change has been applied to this superseding PR: #552

@blueprint.route("/workflows/<workflow_id_or_name>/share", methods=["POST"])
@use_kwargs(
{
"user_id": fields.Str(required=True),
Copy link
Member

Choose a reason for hiding this comment

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

Let's change the name of the parameter user_id to user, to be consistent with the other endpoints. It needs to be changed here, in the OpenAPI spec and in the function body

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

The change has been applied to this superseding PR: #552

Comment on lines +900 to +902
"user_email_to_share_with": fields.Str(required=True),
"message": fields.Str(required=False),
"valid_until": fields.Str(required=False),
Copy link
Member

Choose a reason for hiding this comment

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

I would personally put these parameters (user_email_to_share_with, message and valid_until) in the request body instead of sending them as query parameters, for two reasons:

  • message can be very long, and having long URLs can cause troubles
  • this is a POST request, and the details of the "resource" (in this case, the sharing request) that needs to be created are usually passed as part of the body

What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree with you. Thanks for the suggestion.

The change has been applied to this superseding PR: #552

Comment on lines +978 to +981
errors:
type: array
items:
type: string
Copy link
Member

Choose a reason for hiding this comment

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

Please correct me if I am wrong, but I think we never return in any other endpoint an array of errors. If this is not needed, let's stick to the usual way of just returning a message, to be consistent with the rest.

What do you think? This needs to be changed also in the specification of the other return codes.

Copy link
Member Author

Choose a reason for hiding this comment

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

You are right, nicely spotted.

The change has been applied to this superseding PR: #552

Comment on lines +991 to +1000
schema:
type: object
properties:
message:
type: string
examples:
application/json:
{
"errors": ["User is not allowed to share the workflow."]
}
Copy link
Member

Choose a reason for hiding this comment

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

Schema and examples do not match

Copy link
Member Author

Choose a reason for hiding this comment

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

The 403 has been removed, see this comment: #538 (comment)

@use_kwargs(
{
"user_id": fields.Str(required=True),
"user_email_to_share_with": fields.Str(required=True),
Copy link
Member

Choose a reason for hiding this comment

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

We could even use fields.Email here, but it's not strictly necessary as we are only using it for lookups (and we are storing it only if an user with that email exists): https://marshmallow.readthedocs.io/en/stable/marshmallow.fields.html#marshmallow.fields.Email

Copy link
Member Author

@DaanRosendal DaanRosendal Mar 20, 2024

Choose a reason for hiding this comment

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

Idem. Having refactored user_email_to_share_with to be in the JSON request body, is this still possible?

raise ValueError("Unable to share a workflow with yourself.")

user_to_share_with = (
Session.query(User).filter(User.email == user_email_to_share_with).first()
Copy link
Member

Choose a reason for hiding this comment

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

minor: we already have an unique index on email so it does not really matter, but to be pedantic this should be one_or_none() and not first(), to make sure there is only one user returned by the query.

Not really needed to change it here, but I personally like it more as it helps to find wrong SQL queries where multiple rows are returned while we expected only one of them. A common mistake is to forget one filtering condition and you are suddenly returning the wrong data, or even worse applying actions to the wrong user/workflow! (this is not the case here, though)

Copy link
Member Author

Choose a reason for hiding this comment

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

I changed it to be one_or_none()

The change has been applied to this superseding PR: #552


if valid_until:
try:
datetime.date.fromisoformat(valid_until)
Copy link
Member

Choose a reason for hiding this comment

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

Parsing the date should not be needed here if we define it as fields.Date, see the previous related comment

Copy link
Member Author

@DaanRosendal DaanRosendal Mar 20, 2024

Choose a reason for hiding this comment

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

Idem. Having refactored valid_until to be in the JSON request body, this check should stay if there is no way to use the fields.Date field.

if datetime.date.fromisoformat(valid_until) < datetime.date.today():
raise ValueError("The 'valid_until' date cannot be in the past.")

if message and len(message) > 5000:
Copy link
Member

Choose a reason for hiding this comment

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

A couple of comments:

  • we can validate the length of the string already inside use_kwargs, like this (not tested):
"message": fields.Str(required=False, validate=validate.Length(max=5000))
  • I think we should put this value in config.py (but no need to read it from an env variable) to avoid having 5000 hardcoded in the code

What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

Idem. Having refactored message to be in the JSON request body, is this still possible?

I did create a config value MAX_WORKFLOW_SHARING_MESSAGE_LENGTH. The change has been applied to this superseding PR: #552

Comment on lines +1111 to +1135
existing_share = (
Session.query(UserWorkflow)
.filter_by(user_id=user_to_share_with.id_, workflow_id=workflow.id_)
.first()
)

if existing_share:
return (
jsonify(
{
"message": f"{workflow.get_full_workflow_name()} is already shared with {user_email_to_share_with}."
}
),
409,
)

Session.add(
UserWorkflow(
user_id=user_to_share_with.id_,
workflow_id=workflow.id_,
message=message,
valid_until=valid_until,
)
)
Session.commit()
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 a potential race condition here, as this can happen:

  1. existing_share is fetched from the database, but it does not exist so None is returned
  2. another (concurrent) API request creates the workflow share
  3. we try to create a new share, but Session.add/Session.commit will fail as the share already exists

Note that in this case nothing bad happens, as the database will simply return an integrity error (given that there will be multiple rows with the same primary keys). However, we could prevent this issue by simply inserting the new row without checking first if it exists:

try:
    Session.add(UserWorkflow(...))
    Session.commit()
except IntegrityError:
    Session.rollback()
    return jsonify(...), 409

Also see https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use

Copy link
Member Author

Choose a reason for hiding this comment

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

Nicely spotted! 👍

The change has been applied to this superseding PR: #552

"message": "Malformed request.",
"errors": ["Missing data for required field."]
}
403:
Copy link
Member

Choose a reason for hiding this comment

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

I think the 403 error is never returned?

Copy link
Member Author

Choose a reason for hiding this comment

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

That is correct, nicely spotted. I removed it. In the future once this is implemented (reanahub/reana-server#663) it could be readded.

The change has been applied to this superseding PR: #552

@mdonadoni
Copy link
Member

Included in #552

@mdonadoni mdonadoni closed this Aug 20, 2024
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.

cli: implement new share-add command
2 participants