Skip to content

Commit

Permalink
feat(rest): add endpoint to share workflows (#552)
Browse files Browse the repository at this point in the history
Adds a new endpoint to share a workflow with a user.

Closes reanahub/reana-client#680
  • Loading branch information
DaanRosendal authored and tiborsimko committed Sep 4, 2024
1 parent de3c933 commit a2ccc64
Show file tree
Hide file tree
Showing 5 changed files with 682 additions and 16 deletions.
1 change: 1 addition & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The list of contributors in alphabetical order:
- [Anton Khodak](https://orcid.org/0000-0003-3263-4553)
- [Audrius Mecionis](https://orcid.org/0000-0002-3759-1663)
- [Camila Diaz](https://orcid.org/0000-0001-5543-797X)
- [Daan Rosendal](https://orcid.org/0000-0002-3447-9000)
- [Diego Rodriguez](https://orcid.org/0000-0003-0649-2002)
- [Dinos Kousidis](https://orcid.org/0000-0002-4914-4289)
- [Giuseppe Steduto](https://orcid.org/0009-0002-1258-8553)
Expand Down
105 changes: 105 additions & 0 deletions docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,111 @@
"summary": "Get the retention rules of a workflow."
}
},
"/api/workflows/{workflow_id_or_name}/share": {
"post": {
"description": "This resource allows to share a workflow with other users.",
"operationId": "share_workflow",
"parameters": [
{
"description": "Required. UUID of workflow owner.",
"in": "query",
"name": "user",
"required": true,
"type": "string"
},
{
"description": "Required. Analysis UUID or name.",
"in": "path",
"name": "workflow_id_or_name",
"required": true,
"type": "string"
},
{
"description": "JSON object with details of the share.",
"in": "body",
"name": "share_details",
"required": true,
"schema": {
"properties": {
"message": {
"description": "Optional. Message to include when sharing the workflow.",
"type": "string"
},
"user_email_to_share_with": {
"description": "User to share the workflow with.",
"type": "string"
},
"valid_until": {
"description": "Optional. Date when access to the workflow will expire (format YYYY-MM-DD).",
"type": "string"
}
},
"required": [
"user_email_to_share_with"
],
"type": "object"
}
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "Request succeeded. The workflow has been shared with the user.",
"examples": {
"application/json": {
"message": "The workflow has been shared with the user.",
"workflow_id": "cdcf48b1-c2f3-4693-8230-b066e088c6ac",
"workflow_name": "mytest.1"
}
},
"schema": {
"properties": {
"message": {
"type": "string"
},
"workflow_id": {
"type": "string"
},
"workflow_name": {
"type": "string"
}
},
"type": "object"
}
},
"400": {
"description": "Request failed. The incoming data seems malformed."
},
"404": {
"description": "Request failed. Workflow does not exist or user does not exist.",
"examples": {
"application/json": {
"message": "Workflow cdcf48b1-c2f3-4693-8230-b066e088c6ac does not exist"
}
}
},
"409": {
"description": "Request failed. The workflow is already shared with the user.",
"examples": {
"application/json": {
"message": "The workflow is already shared with the user."
}
}
},
"500": {
"description": "Request failed. Internal controller error.",
"examples": {
"application/json": {
"message": "Internal controller error."
}
}
}
},
"summary": "Share a workflow with other users."
}
},
"/api/workflows/{workflow_id_or_name}/status": {
"get": {
"description": "This resource reports the status of workflow.",
Expand Down
2 changes: 2 additions & 0 deletions reana_workflow_controller/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,5 @@ def _parse_interactive_sessions_environments(env_var):

CONTAINER_IMAGE_ALIAS_PREFIXES = ["docker.io/", "docker.io/library/", "library/"]
"""Prefixes that can be removed from container image references to generate valid image aliases."""

MAX_WORKFLOW_SHARING_MESSAGE_LENGTH = 5000
207 changes: 196 additions & 11 deletions reana_workflow_controller/rest/workflows.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
# -*- coding: utf-8 -*-
#
# This file is part of REANA.
# Copyright (C) 2020, 2021, 2022 CERN.
# Copyright (C) 2020, 2021, 2022, 2023 CERN.
#
# REANA is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""REANA Workflow Controller workflows REST API."""

import datetime
import json
import logging
import re
from typing import Optional
from uuid import uuid4

from flask import Blueprint, jsonify, request
from reana_commons.config import WORKFLOW_TIME_FORMAT
from reana_db.database import Session
from reana_db.utils import build_workspace_path
from reana_db.models import User, Workflow, RunStatus, WorkflowResource
from reana_db.utils import _get_workflow_with_uuid_or_name, get_default_quota_resource
from sqlalchemy import and_, nullslast
from sqlalchemy import and_, nullslast, or_
from sqlalchemy.orm import aliased
from sqlalchemy.exc import IntegrityError
from webargs import fields
from webargs.flaskparser import use_args, use_kwargs


from reana_workflow_controller.config import DEFAULT_NAME_FOR_WORKFLOWS
from reana_commons.config import WORKFLOW_TIME_FORMAT
from reana_db.database import Session
from reana_db.models import RunStatus, User, UserWorkflow, Workflow, WorkflowResource
from reana_db.utils import (
_get_workflow_with_uuid_or_name,
build_workspace_path,
get_default_quota_resource,
)
from reana_workflow_controller.config import (
DEFAULT_NAME_FOR_WORKFLOWS,
MAX_WORKFLOW_SHARING_MESSAGE_LENGTH,
)
from reana_workflow_controller.errors import (
REANAWorkflowControllerError,
REANAWorkflowNameError,
Expand All @@ -39,7 +47,6 @@
use_paginate_args,
)


START = "start"
STOP = "stop"
DELETED = "deleted"
Expand Down Expand Up @@ -888,3 +895,181 @@ def get_workflow_retention_rules(workflow_id_or_name: str, user: str):
except Exception as e:
logging.exception(str(e))
return jsonify({"message": str(e)}), 500


@blueprint.route("/workflows/<workflow_id_or_name>/share", methods=["POST"])
@use_kwargs({"user": fields.Str(required=True)}, location="query")
def share_workflow(workflow_id_or_name: str, user: str):
r"""Share a workflow with other users.
---
post:
summary: Share a workflow with other users.
description: >-
This resource allows to share a workflow with other users.
operationId: share_workflow
produces:
- application/json
parameters:
- name: user
in: query
description: Required. UUID of workflow owner.
required: true
type: string
- name: workflow_id_or_name
in: path
description: Required. Analysis UUID or name.
required: true
type: string
- name: share_details
in: body
description: JSON object with details of the share.
required: true
schema:
type: object
properties:
user_email_to_share_with:
type: string
description: User to share the workflow with.
message:
type: string
description: Optional. Message to include when sharing the workflow.
valid_until:
type: string
description: Optional. Date when access to the workflow will expire (format YYYY-MM-DD).
required: [user_email_to_share_with]
responses:
200:
description: >-
Request succeeded. The workflow has been shared with the user.
schema:
type: object
properties:
message:
type: string
workflow_id:
type: string
workflow_name:
type: string
examples:
application/json:
{
"message": "The workflow has been shared with the user.",
"workflow_id": "cdcf48b1-c2f3-4693-8230-b066e088c6ac",
"workflow_name": "mytest.1"
}
400:
description: >-
Request failed. The incoming data seems malformed.
404:
description: >-
Request failed. Workflow does not exist or user does not exist.
examples:
application/json:
{
"message": "Workflow cdcf48b1-c2f3-4693-8230-b066e088c6ac does
not exist",
}
409:
description: >-
Request failed. The workflow is already shared with the user.
examples:
application/json:
{
"message": "The workflow is already shared with the user.",
}
500:
description: >-
Request failed. Internal controller error.
examples:
application/json:
{
"message": "Internal controller error.",
}
"""
try:
sharer = User.query.filter(User.id_ == user).first()
if not sharer:
return (
jsonify({"message": f"User with id '{user}' does not exist."}),
404,
)

share_details = request.json
user_email_to_share_with = share_details.get("user_email_to_share_with")
message = share_details.get("message", None)
valid_until = share_details.get("valid_until", None)

if sharer.email == user_email_to_share_with:
raise ValueError("Unable to share a workflow with yourself.")

user_to_share_with = (
Session.query(User)
.filter(User.email == user_email_to_share_with)
.one_or_none()
)

if not user_to_share_with:
return (
jsonify(
{
"message": f"User with email '{user_email_to_share_with}' does not exist."
}
),
404,
)

if valid_until:
try:
datetime.date.fromisoformat(valid_until)
except ValueError as e:
raise ValueError(
f"Date format is not valid ({str(e)}). Please use YYYY-MM-DD format."
)

# check if date is in the future
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) > MAX_WORKFLOW_SHARING_MESSAGE_LENGTH:
raise ValueError(
"Message is too long. Please keep it under "
+ str(MAX_WORKFLOW_SHARING_MESSAGE_LENGTH)
+ " characters."
)

workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, sharer.id_)

try:
Session.add(
UserWorkflow(
user_id=user_to_share_with.id_,
workflow_id=workflow.id_,
message=message,
valid_until=valid_until,
)
)
Session.commit()
except IntegrityError:
Session.rollback()
return (
jsonify(
{
"message": f"{workflow.get_full_workflow_name()} is already shared with {user_email_to_share_with}."
}
),
409,
)

response = {
"message": "The workflow has been shared with the user.",
"workflow_id": workflow.id_,
"workflow_name": workflow.get_full_workflow_name(),
}
return jsonify(response), 200
except ValueError as e:
logging.exception(str(e))
return jsonify({"message": str(e)}), 400
except Exception as e:
logging.exception(str(e))
return jsonify({"message": str(e)}), 500
Loading

0 comments on commit a2ccc64

Please sign in to comment.