From 1f608f6b985569aa5e85294afec3b309160c4c85 Mon Sep 17 00:00:00 2001 From: Daan Rosendal Date: Sat, 4 Nov 2023 15:39:36 +0100 Subject: [PATCH 1/4] rest: add share_workflow endpoint Adds a new endpoint to share a workflow with a user. Closes reanahub/reana-client#680 --- docs/openapi.json | 191 ++++++++++++++++++++++++++++ reana_server/rest/workflows.py | 221 ++++++++++++++++++++++++++++++--- 2 files changed, 394 insertions(+), 18 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index 436a4250..36d3fad5 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2972,6 +2972,197 @@ "summary": "Get the retention rules of a workflow." } }, + "/api/workflows/{workflow_id_or_name}/share": { + "post": { + "description": "This resource shares a workflow with another user. This resource is expecting a workflow UUID and some parameters.", + "operationId": "share_workflow", + "parameters": [ + { + "description": "The API access_token of workflow owner.", + "in": "query", + "name": "access_token", + "required": false, + "type": "string" + }, + { + "description": "Required. Workflow UUID or name.", + "in": "path", + "name": "workflow_id_or_name", + "required": true, + "type": "string" + }, + { + "description": "Required. User to share the workflow with.", + "in": "query", + "name": "user_email_to_share_with", + "required": true, + "type": "string" + }, + { + "description": "Optional. Message to include when sharing the workflow.", + "in": "query", + "name": "message", + "required": false, + "type": "string" + }, + { + "description": "Optional. Date when access to the workflow will expire (format YYYY-MM-DD).", + "in": "query", + "name": "valid_until", + "required": false, + "type": "string" + } + ], + "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 specification seems malformed.", + "examples": { + "application/json": { + "errors": [ + "Missing data for required field." + ], + "message": "Malformed request." + } + }, + "schema": { + "properties": { + "errors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "403": { + "description": "Request failed. User is not allowed to share the workflow.", + "examples": { + "application/json": { + "errors": [ + "User is not allowed to share the workflow." + ] + } + }, + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "404": { + "description": "Request failed. Workflow does not exist or user does not exist.", + "examples": { + "application/json": { + "errors": [ + "Workflow cdcf48b1-c2f3-4693-8230-b066e088c6ac does not exist" + ], + "message": "Workflow cdcf48b1-c2f3-4693-8230-b066e088c6ac does not exist" + } + }, + "schema": { + "properties": { + "errors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "409": { + "description": "Request failed. The workflow is already shared with the user.", + "examples": { + "application/json": { + "errors": [ + "The workflow is already shared with the user." + ], + "message": "The workflow is already shared with the user." + } + }, + "schema": { + "properties": { + "errors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "500": { + "description": "Request failed. Internal controller error.", + "examples": { + "application/json": { + "errors": [ + "Internal controller error." + ], + "message": "Internal controller error." + } + }, + "schema": { + "properties": { + "errors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "summary": "Share a workflow with another user." + } + }, "/api/workflows/{workflow_id_or_name}/specification": { "get": { "description": "This resource returns the REANA workflow specification used to start the workflow run. Resource is expecting a workflow UUID.", diff --git a/reana_server/rest/workflows.py b/reana_server/rest/workflows.py index de6f7861..2f01f42d 100644 --- a/reana_server/rest/workflows.py +++ b/reana_server/rest/workflows.py @@ -1,56 +1,53 @@ # -*- coding: utf-8 -*- # # This file is part of REANA. -# Copyright (C) 2018, 2019, 2020, 2021, 2022 CERN. +# Copyright (C) 2018, 2019, 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-Server workflow-functionality Flask-Blueprint.""" -import os import json import logging +import os import traceback import requests from bravado.exception import HTTPError -from flask import Blueprint, Response -from flask import jsonify, request, stream_with_context +from flask import Blueprint, Response, jsonify, request, stream_with_context from jsonschema.exceptions import ValidationError - from reana_commons import workspace from reana_commons.config import REANA_WORKFLOW_ENGINES from reana_commons.errors import REANAQuotaExceededError, REANAValidationError +from reana_commons.specification import load_reana_spec from reana_commons.validation.operational_options import validate_operational_options from reana_commons.validation.utils import validate_workflow_name -from reana_commons.specification import load_reana_spec from reana_db.database import Session from reana_db.models import InteractiveSessionType, RunStatus from reana_db.utils import _get_workflow_with_uuid_or_name -from webargs import fields, validate -from webargs.flaskparser import use_kwargs - from reana_server.api_client import current_rwc_api_client from reana_server.config import REANA_HOSTNAME from reana_server.decorators import check_quota, signin_required from reana_server.deleter import Deleter, InOrOut -from reana_server.validation import ( - validate_inputs, - validate_workspace_path, - validate_workflow, -) from reana_server.utils import ( - _fail_gitlab_commit_build_status, RequestStreamWithLen, - _load_and_save_yadage_spec, + _fail_gitlab_commit_build_status, _get_reana_yaml_from_gitlab, - prevent_disk_quota_excess, - publish_workflow_submission, + _load_and_save_yadage_spec, clone_workflow, get_quota_excess_message, get_workspace_retention_rules, is_uuid_v4, + prevent_disk_quota_excess, + publish_workflow_submission, ) +from reana_server.validation import ( + validate_inputs, + validate_workflow, + validate_workspace_path, +) +from webargs import fields, validate +from webargs.flaskparser import use_kwargs try: from urllib import parse as urlparse @@ -3251,3 +3248,191 @@ def prune_workspace( except Exception as e: logging.exception(str(e)) return jsonify({"message": str(e)}), 500 + + +@blueprint.route("/workflows//share", methods=["POST"]) +@use_kwargs( + { + "user_email_to_share_with": fields.String(), + "message": fields.String(missing=None), + "valid_until": fields.String(missing=None), + } +) +@signin_required() +def share_workflow( + workflow_id_or_name, user, user_email_to_share_with, message, valid_until +): + r"""Share a workflow with another user. + + --- + post: + summary: Share a workflow with another user. + description: >- + This resource shares a workflow with another user. + This resource is expecting a workflow UUID and some parameters. + operationId: share_workflow + produces: + - application/json + parameters: + - name: access_token + in: query + description: The API access_token of workflow owner. + required: false + type: string + - name: workflow_id_or_name + in: path + description: Required. Workflow UUID or name. + required: true + type: string + - name: user_email_to_share_with + in: query + description: >- + Required. User to share the workflow with. + required: true + type: string + - name: message + in: query + description: Optional. Message to include when sharing the workflow. + required: false + type: string + - name: valid_until + in: query + description: Optional. Date when access to the workflow will expire (format YYYY-MM-DD). + required: false + type: string + 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 specification seems malformed. + schema: + type: object + properties: + message: + type: string + errors: + type: array + items: + type: string + examples: + application/json: + { + "message": "Malformed request.", + "errors": ["Missing data for required field."] + } + 403: + description: >- + Request failed. User is not allowed to share the workflow. + schema: + type: object + properties: + message: + type: string + examples: + application/json: + { + "errors": ["User is not allowed to share the workflow."] + } + 404: + description: >- + Request failed. Workflow does not exist or user does not exist. + schema: + type: object + properties: + message: + type: string + errors: + type: array + items: + type: string + examples: + application/json: + { + "message": "Workflow cdcf48b1-c2f3-4693-8230-b066e088c6ac does + not exist", + "errors": ["Workflow cdcf48b1-c2f3-4693-8230-b066e088c6ac does + not exist"] + } + 409: + description: >- + Request failed. The workflow is already shared with the user. + schema: + type: object + properties: + message: + type: string + errors: + type: array + items: + type: string + examples: + application/json: + { + "message": "The workflow is already shared with the user.", + "errors": ["The workflow is already shared with the user."] + } + 500: + description: >- + Request failed. Internal controller error. + schema: + type: object + properties: + message: + type: string + errors: + type: array + items: + type: string + examples: + application/json: + { + "message": "Internal controller error.", + "errors": ["Internal controller error."] + } + """ + try: + share_params = { + "workflow_id_or_name": workflow_id_or_name, + "user_email_to_share_with": user_email_to_share_with, + "user_id": str(user.id_), + } + + if message: + share_params["message"] = message + + if valid_until: + share_params["valid_until"] = valid_until + + response, http_response = current_rwc_api_client.api.share_workflow( + **share_params + ).result() + + return jsonify(response), 200 + except HTTPError as e: + logging.exception(str(e)) + return jsonify(e.response.json()), e.response.status_code + except ValueError as e: + # In case of invalid workflow name / UUID + logging.exception(str(e)) + return jsonify({"message": str(e)}), 403 + except Exception as e: + logging.exception(str(e)) + return jsonify({"message": str(e)}), 500 From 4e05bae477a30783e405ba0428d73fe750a59da3 Mon Sep 17 00:00:00 2001 From: Daan Rosendal Date: Thu, 9 Nov 2023 15:24:53 +0100 Subject: [PATCH 2/4] tests: rename default_user to user0 --- tests/test_cli.py | 75 ++++++++++++++--------------- tests/test_decorators.py | 9 ++-- tests/test_deleter.py | 7 ++- tests/test_utils.py | 20 ++++---- tests/test_views.py | 101 +++++++++++++++++++-------------------- 5 files changed, 104 insertions(+), 108 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index f2932c05..b8c8ad58 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -31,7 +31,6 @@ WorkspaceRetentionRuleStatus, generate_uuid, ) - from reana_server.api_client import WorkflowSubmissionPublisher from reana_server.reana_admin import reana_admin from reana_server.reana_admin.check_workflows import check_workspaces @@ -39,27 +38,27 @@ from reana_server.reana_admin.consumer import MessageConsumer -def test_export_users(default_user): +def test_export_users(user0): """Test exporting all users as csv.""" runner = CliRunner() expected_csv_file = io.StringIO() csv_writer = csv.writer(expected_csv_file, dialect="unix") csv_writer.writerow( [ - default_user.id_, - default_user.email, - default_user.access_token, - default_user.username, - default_user.full_name, + user0.id_, + user0.email, + user0.access_token, + user0.username, + user0.full_name, ] ) result = runner.invoke( - reana_admin, ["user-export", "--admin-access-token", default_user.access_token] + reana_admin, ["user-export", "--admin-access-token", user0.access_token] ) assert result.output == expected_csv_file.getvalue() -def test_import_users(app, session, default_user): +def test_import_users(app, session, user0): """Test importing users from CSV file.""" runner = CliRunner() expected_output = "Users successfully imported." @@ -81,7 +80,7 @@ def test_import_users(app, session, default_user): [ "user-import", "--admin-access-token", - default_user.access_token, + user0.access_token, "--file", users_csv_file_name, ], @@ -95,7 +94,7 @@ def test_import_users(app, session, default_user): assert user.full_name == user_full_name -def test_grant_token(default_user, session): +def test_grant_token(user0, session): """Test grant access token.""" runner = CliRunner() @@ -105,7 +104,7 @@ def test_grant_token(default_user, session): [ "token-grant", "--admin-access-token", - default_user.access_token, + user0.access_token, "-e", "nonexisting@example.org", ], @@ -118,7 +117,7 @@ def test_grant_token(default_user, session): [ "token-grant", "--admin-access-token", - default_user.access_token, + user0.access_token, "--id", "fake_id", ], @@ -134,7 +133,7 @@ def test_grant_token(default_user, session): [ "token-grant", "--admin-access-token", - default_user.access_token, + user0.access_token, "-e", user.email, ], @@ -147,7 +146,7 @@ def test_grant_token(default_user, session): [ "token-grant", "--admin-access-token", - default_user.access_token, + user0.access_token, "-e", user.email, ], @@ -161,7 +160,7 @@ def test_grant_token(default_user, session): [ "token-grant", "--admin-access-token", - default_user.access_token, + user0.access_token, "-e", user.email, ], @@ -169,7 +168,7 @@ def test_grant_token(default_user, session): ) assert f"Token for user {user.id_} ({user.email}) granted" in result.output assert user.access_token - assert default_user.audit_logs[-1].action is AuditLogAction.grant_token + assert user0.audit_logs[-1].action is AuditLogAction.grant_token # user with active token active_user = User(email="active@cern.ch", access_token="valid_token") @@ -180,7 +179,7 @@ def test_grant_token(default_user, session): [ "token-grant", "--admin-access-token", - default_user.access_token, + user0.access_token, "--id", str(active_user.id_), ], @@ -199,17 +198,17 @@ def test_grant_token(default_user, session): [ "token-grant", "--admin-access-token", - default_user.access_token, + user0.access_token, "--id", str(ui_user.id_), ], ) assert ui_user.access_token_status is UserTokenStatus.active.name assert ui_user.access_token - assert default_user.audit_logs[-1].action is AuditLogAction.grant_token + assert user0.audit_logs[-1].action is AuditLogAction.grant_token -def test_revoke_token(default_user, session): +def test_revoke_token(user0, session): """Test revoke access token.""" runner = CliRunner() @@ -222,7 +221,7 @@ def test_revoke_token(default_user, session): [ "token-revoke", "--admin-access-token", - default_user.access_token, + user0.access_token, "-e", user.email, ], @@ -237,7 +236,7 @@ def test_revoke_token(default_user, session): [ "token-revoke", "--admin-access-token", - default_user.access_token, + user0.access_token, "-e", user.email, ], @@ -253,14 +252,14 @@ def test_revoke_token(default_user, session): [ "token-revoke", "--admin-access-token", - default_user.access_token, + user0.access_token, "--id", str(user.id_), ], ) assert "was successfully revoked" in result.output assert user.access_token_status == UserTokenStatus.revoked.name - assert default_user.audit_logs[-1].action is AuditLogAction.revoke_token + assert user0.audit_logs[-1].action is AuditLogAction.revoke_token # try to revoke again result = runner.invoke( @@ -268,7 +267,7 @@ def test_revoke_token(default_user, session): [ "token-revoke", "--admin-access-token", - default_user.access_token, + user0.access_token, "--id", str(user.id_), ], @@ -398,7 +397,7 @@ def test_is_input_or_output(file_or_dir, expected_result): ], ) def test_retention_rules_apply( - default_user, + user0, workflow_with_retention_rules, session, time_delta, @@ -437,7 +436,7 @@ def init_workspace(workspace, files): command = [ "retention-rules-apply", "--admin-access-token", - default_user.access_token, + user0.access_token, ] if time_delta is not None: forced_date = datetime.datetime.now() + time_delta @@ -472,7 +471,7 @@ def init_workspace(workspace, files): @patch("reana_server.reana_admin.cli.RetentionRuleDeleter.apply_rule") def test_retention_rules_apply_error( - apply_rule_mock: Mock, workflow_with_retention_rules, default_user + apply_rule_mock: Mock, workflow_with_retention_rules, user0 ): """Test that rules are reset to `active` if there are errors.""" workflow = workflow_with_retention_rules @@ -484,7 +483,7 @@ def test_retention_rules_apply_error( [ "retention-rules-apply", "--admin-access-token", - default_user.access_token, + user0.access_token, ], ) @@ -495,7 +494,7 @@ def test_retention_rules_apply_error( assert rule.status == WorkspaceRetentionRuleStatus.active -def test_retention_rules_extend(workflow_with_retention_rules, default_user): +def test_retention_rules_extend(workflow_with_retention_rules, user0): """Test extending of retention rules.""" workflow = workflow_with_retention_rules runner = CliRunner() @@ -509,7 +508,7 @@ def test_retention_rules_extend(workflow_with_retention_rules, default_user): "-d", extend_days, "--admin-access-token", - default_user.access_token, + user0.access_token, ], ) assert result.output == "Invalid workflow UUID.\n" @@ -524,7 +523,7 @@ def test_retention_rules_extend(workflow_with_retention_rules, default_user): "-d", extend_days, "--admin-access-token", - default_user.access_token, + user0.access_token, ], ) assert "Extending rule" in result.output @@ -559,7 +558,7 @@ def test_retention_rule_deleter_file_outside_workspace(tmp_path): ) @patch("reana_server.reana_admin.cli.requests.get") def test_interactive_session_cleanup( - mock_requests, sample_serial_workflow_in_db, days, output, default_user + mock_requests, sample_serial_workflow_in_db, days, output, user0 ): """Test closure of long running interactive sessions.""" runner = CliRunner() @@ -602,7 +601,7 @@ def test_interactive_session_cleanup( "-d", days, "--admin-access-token", - default_user.access_token, + user0.access_token, ], ) assert output in result.output @@ -972,7 +971,7 @@ def test_check_workspaces( assert any(workflow.workspace_path in str(error) for error in result.errors) -def test_quota_set_default_limits_for_user_with_custom_limits(default_user, session): +def test_quota_set_default_limits_for_user_with_custom_limits(user0, session): """Test setting default quota when there are is one user with custom quota limits.""" runner = CliRunner() @@ -981,7 +980,7 @@ def test_quota_set_default_limits_for_user_with_custom_limits(default_user, sess for resource in resources: user_resource = ( session.query(UserResource) - .filter_by(user_id=default_user.id_, resource_id=resource.id_) + .filter_by(user_id=user0.id_, resource_id=resource.id_) .first() ) @@ -995,7 +994,7 @@ def test_quota_set_default_limits_for_user_with_custom_limits(default_user, sess [ "quota-set-default-limits", "--admin-access-token", - default_user.access_token, + user0.access_token, ], ) diff --git a/tests/test_decorators.py b/tests/test_decorators.py index f2e52235..8e975b3f 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -7,24 +7,23 @@ # under the terms of the MIT License; see LICENSE file for more details. """REANA-Server decorators tests.""" -from flask import jsonify import json from unittest.mock import Mock, patch +from flask import jsonify from reana_db.models import User, UserToken - from reana_server.decorators import signin_required -def test_signing_required_with_token(default_user: User): +def test_signing_required_with_token(user0: User): """Test `signin_required` when user does not have a valid token.""" # Delete user tokens - UserToken.query.filter(UserToken.user_id == default_user.id_).delete() + UserToken.query.filter(UserToken.user_id == user0.id_).delete() mock_endpoint = Mock(return_value=(jsonify(message="Ok"), 200)) mock_current_user = Mock() mock_current_user.is_authenticated = True - mock_current_user.email = default_user.email + mock_current_user.email = user0.email with patch("reana_server.decorators.current_user", mock_current_user): decorated_endpoint = signin_required()(mock_endpoint) response, code = decorated_endpoint() diff --git a/tests/test_deleter.py b/tests/test_deleter.py index ae0049c1..f731a0c2 100644 --- a/tests/test_deleter.py +++ b/tests/test_deleter.py @@ -6,14 +6,13 @@ # 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-Server deleter tests.""" -import pytest import pathlib import uuid +import pytest import wdb - +from reana_commons.workspace import is_directory, iterdir, walk from reana_server.deleter import Deleter, InOrOut -from reana_commons.workspace import iterdir, is_directory, walk @pytest.mark.parametrize( @@ -65,7 +64,7 @@ ], ) def test_file_deletion( - initial_list, which_to_keep, final_list, default_user, sample_serial_workflow_in_db + initial_list, which_to_keep, final_list, user0, sample_serial_workflow_in_db ): """Test delete files preserving inputs/outputs/none""" diff --git a/tests/test_utils.py b/tests/test_utils.py index ef17d690..f24db86c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -7,11 +7,11 @@ """REANA-Server tests for utils module.""" import pathlib -import pytest +import pytest from reana_commons.errors import REANAValidationError from reana_db.models import UserToken, UserTokenStatus, UserTokenType -from reana_server.utils import is_valid_email, filter_input_files, get_user_from_token +from reana_server.utils import filter_input_files, get_user_from_token, is_valid_email @pytest.mark.parametrize( @@ -49,27 +49,27 @@ def test_filter_input_files(tmp_path: pathlib.Path): assert len(list(tmp_path.iterdir())) == 1 -def test_get_user_from_token(default_user): +def test_get_user_from_token(user0): """Test getting user from his own token.""" - assert default_user.id_ == get_user_from_token(default_user.access_token).id_ + assert user0.id_ == get_user_from_token(user0.access_token).id_ -def test_get_user_from_token_after_revocation(default_user, session): +def test_get_user_from_token_after_revocation(user0, session): """Test getting user from revoked token.""" - token = default_user.active_token + token = user0.active_token token.status = UserTokenStatus.revoked session.commit() with pytest.raises(ValueError, match="revoked"): get_user_from_token(token.token) -def test_get_user_from_token_two_tokens(default_user, session): +def test_get_user_from_token_two_tokens(user0, session): """Test getting user with multiple tokens.""" - old_token = default_user.active_token + old_token = user0.active_token old_token.status = UserTokenStatus.revoked new_token = UserToken( token="new_token", - user_id=default_user.id_, + user_id=user0.id_, type_=UserTokenType.reana, status=UserTokenStatus.active, ) @@ -77,7 +77,7 @@ def test_get_user_from_token_two_tokens(default_user, session): session.commit() # Check that new token works - assert default_user.id_ == get_user_from_token(new_token.token).id_ + assert user0.id_ == get_user_from_token(new_token.token).id_ # Check that old revoked token does not work with pytest.raises(ValueError, match="revoked"): get_user_from_token(old_token.token) diff --git a/tests/test_views.py b/tests/test_views.py index 5db68a3f..030ed443 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -18,15 +18,14 @@ from flask import url_for from mock import Mock, patch from pytest_reana.test_utils import make_mock_api_client -from reana_db.models import User, InteractiveSessionType, RunStatus - +from reana_db.models import InteractiveSessionType, RunStatus, User from reana_server.utils import ( _create_and_associate_local_user, _create_and_associate_oauth_user, ) -def test_get_workflows(app, default_user, _get_user_mock): +def test_get_workflows(app, user0, _get_user_mock): """Test get_workflows view.""" with app.test_client() as client: with patch( @@ -48,7 +47,7 @@ def test_get_workflows(app, default_user, _get_user_mock): res = client.get( url_for("workflows.get_workflows"), query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, "type": "batch", }, ) @@ -56,7 +55,7 @@ def test_get_workflows(app, default_user, _get_user_mock): def test_create_workflow( - app, session, default_user, _get_user_mock, sample_serial_workflow_in_db + app, session, user0, _get_user_mock, sample_serial_workflow_in_db ): """Test create_workflow view.""" with app.test_client() as client: @@ -79,7 +78,7 @@ def test_create_workflow( res = client.post( url_for("workflows.create_workflow"), query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, "spec": "not_implemented", }, ) @@ -88,7 +87,7 @@ def test_create_workflow( # no specification provided res = client.post( url_for("workflows.create_workflow"), - query_string={"access_token": default_user.access_token}, + query_string={"access_token": user0.access_token}, ) assert res.status_code == 500 @@ -101,7 +100,7 @@ def test_create_workflow( url_for("workflows.create_workflow"), headers={"Content-Type": "application/json"}, query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, "workflow_name": "test", }, data=json.dumps(workflow_specification), @@ -113,7 +112,7 @@ def test_create_workflow( url_for("workflows.create_workflow"), headers={"Content-Type": "application/json"}, query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, "workflow_name": str(uuid4()), }, data=json.dumps(sample_serial_workflow_in_db.reana_specification), @@ -128,7 +127,7 @@ def test_create_workflow( url_for("workflows.create_workflow"), headers={"Content-Type": "application/json"}, query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, "workflow_name": "test", }, data=json.dumps(workflow_specification), @@ -143,7 +142,7 @@ def test_create_workflow( url_for("workflows.create_workflow"), headers={"Content-Type": "application/json"}, query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, "workflow_name": "test", }, data=json.dumps(workflow_specification), @@ -156,7 +155,7 @@ def test_create_workflow( url_for("workflows.create_workflow"), headers={"Content-Type": "application/json"}, query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, "workflow_name": "test", }, data=json.dumps(workflow_specification), @@ -165,7 +164,7 @@ def test_create_workflow( def test_start_workflow_validates_specification( - app, session, default_user, sample_serial_workflow_in_db + app, session, user0, sample_serial_workflow_in_db ): with app.test_client() as client: sample_serial_workflow_in_db.status = RunStatus.created @@ -184,7 +183,7 @@ def test_start_workflow_validates_specification( ), headers={"Content-Type": "application/json"}, query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, }, data=json.dumps({}), ) @@ -192,7 +191,7 @@ def test_start_workflow_validates_specification( def test_restart_workflow_validates_specification( - app, session, default_user, sample_serial_workflow_in_db + app, session, user0, sample_serial_workflow_in_db ): with app.test_client() as client: sample_serial_workflow_in_db.status = RunStatus.finished @@ -212,7 +211,7 @@ def test_restart_workflow_validates_specification( url_for("workflows.start_workflow", workflow_id_or_name="test"), headers={"Content-Type": "application/json"}, query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, }, data=json.dumps(body), ) @@ -220,7 +219,7 @@ def test_restart_workflow_validates_specification( def test_get_workflow_specification( - app, default_user, _get_user_mock, sample_yadage_workflow_in_db + app, user0, _get_user_mock, sample_yadage_workflow_in_db ): """Test get_workflow_specification view.""" with app.test_client() as client: @@ -247,7 +246,7 @@ def test_get_workflow_specification( workflow_id_or_name=sample_yadage_workflow_in_db.id_, ), headers={"Content-Type": "application/json"}, - query_string={"access_token": default_user.access_token}, + query_string={"access_token": user0.access_token}, data=json.dumps(None), ) parsed_res = json.loads(res.data) @@ -266,7 +265,7 @@ def test_get_workflow_specification( ) -def test_get_workflow_logs(app, default_user, _get_user_mock): +def test_get_workflow_logs(app, user0, _get_user_mock): """Test get_workflow_logs view.""" with app.test_client() as client: with patch( @@ -287,13 +286,13 @@ def test_get_workflow_logs(app, default_user, _get_user_mock): res = client.get( url_for("workflows.get_workflow_logs", workflow_id_or_name="1"), headers={"Content-Type": "application/json"}, - query_string={"access_token": default_user.access_token}, + query_string={"access_token": user0.access_token}, data=json.dumps(None), ) assert res.status_code == 200 -def test_get_workflow_status(app, default_user, _get_user_mock): +def test_get_workflow_status(app, user0, _get_user_mock): """Test get_workflow_logs view.""" with app.test_client() as client: with patch( @@ -313,12 +312,12 @@ def test_get_workflow_status(app, default_user, _get_user_mock): res = client.get( url_for("workflows.get_workflow_status", workflow_id_or_name="1"), headers={"Content-Type": "application/json"}, - query_string={"access_token": default_user.access_token}, + query_string={"access_token": user0.access_token}, ) assert res.status_code == 200 -def test_set_workflow_status(app, default_user, _get_user_mock): +def test_set_workflow_status(app, user0, _get_user_mock): """Test get_workflow_logs view.""" with app.test_client() as client: with patch( @@ -339,7 +338,7 @@ def test_set_workflow_status(app, default_user, _get_user_mock): res = client.put( url_for("workflows.set_workflow_status", workflow_id_or_name="1"), headers={"Content-Type": "application/json"}, - query_string={"access_token": default_user.access_token}, + query_string={"access_token": user0.access_token}, ) assert res.status_code == 500 @@ -347,7 +346,7 @@ def test_set_workflow_status(app, default_user, _get_user_mock): url_for("workflows.set_workflow_status", workflow_id_or_name="1"), headers={"Content-Type": "application/json"}, query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, "status": "stop", }, data=json.dumps(dict(parameters=None)), @@ -355,7 +354,7 @@ def test_set_workflow_status(app, default_user, _get_user_mock): assert res.status_code == 200 -def test_upload_file(app, default_user, _get_user_mock): +def test_upload_file(app, user0, _get_user_mock): """Test upload_file view.""" with app.test_client() as client: with patch("reana_server.rest.workflows.requests"): @@ -381,7 +380,7 @@ def test_upload_file(app, default_user, _get_user_mock): res = client.post( url_for("workflows.upload_file", workflow_id_or_name="1"), query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, "file_name": "test_upload.txt", }, headers={"Content-Type": "multipart/form-data"}, @@ -392,7 +391,7 @@ def test_upload_file(app, default_user, _get_user_mock): res = client.post( url_for("workflows.upload_file", workflow_id_or_name="1"), query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, "file_name": None, }, headers={"Content-Type": "application/octet-stream"}, @@ -411,7 +410,7 @@ def test_upload_file(app, default_user, _get_user_mock): res = client.post( url_for("workflows.upload_file", workflow_id_or_name="1"), query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, "file_name": "test_upload.txt", }, headers={"Content-Type": "application/octet-stream"}, @@ -426,7 +425,7 @@ def test_upload_file(app, default_user, _get_user_mock): res = client.post( url_for("workflows.upload_file", workflow_id_or_name="1"), query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, "file_name": "empty.txt", }, headers={"Content-Type": "application/octet-stream"}, @@ -438,7 +437,7 @@ def test_upload_file(app, default_user, _get_user_mock): assert not data.read() -def test_download_file(app, default_user, _get_user_mock): +def test_download_file(app, user0, _get_user_mock): """Test download_file view.""" with app.test_client() as client: with patch("reana_server.rest.workflows.requests"): @@ -482,14 +481,14 @@ def test_download_file(app, default_user, _get_user_mock): workflow_id_or_name="1", file_name="test_download", ), - query_string={"access_token": default_user.access_token}, + query_string={"access_token": user0.access_token}, ) requests_client.get.assert_called_once() assert requests_client.get.return_value.status_code == 200 -def test_delete_file(app, default_user, _get_user_mock): +def test_delete_file(app, user0, _get_user_mock): """Test delete_file view.""" mock_response = Mock() mock_response.headers = {"Content-Type": "multipart/form-data"} @@ -529,12 +528,12 @@ def test_delete_file(app, default_user, _get_user_mock): workflow_id_or_name="1", file_name="test_delete.txt", ), - query_string={"access_token": default_user.access_token}, + query_string={"access_token": user0.access_token}, ) assert res.status_code == 200 -def test_get_files(app, default_user, _get_user_mock): +def test_get_files(app, user0, _get_user_mock): """Test get_files view.""" with app.test_client() as client: with patch( @@ -552,7 +551,7 @@ def test_get_files(app, default_user, _get_user_mock): res = client.get( url_for("workflows.get_files", workflow_id_or_name="1"), - query_string={"access_token": default_user.access_token}, + query_string={"access_token": user0.access_token}, ) assert res.status_code == 500 @@ -567,12 +566,12 @@ def test_get_files(app, default_user, _get_user_mock): ): res = client.get( url_for("workflows.get_files", workflow_id_or_name="1"), - query_string={"access_token": default_user.access_token}, + query_string={"access_token": user0.access_token}, ) assert res.status_code == 200 -def test_move_files(app, default_user, _get_user_mock): +def test_move_files(app, user0, _get_user_mock): """Test move_files view.""" with app.test_client() as client: with patch( @@ -582,7 +581,7 @@ def test_move_files(app, default_user, _get_user_mock): res = client.put( url_for("workflows.move_files", workflow_id_or_name="1"), query_string={ - "user": default_user.id_, + "user": user0.id_, "source": "source.txt", "target": "target.txt", }, @@ -592,7 +591,7 @@ def test_move_files(app, default_user, _get_user_mock): res = client.put( url_for("workflows.move_files", workflow_id_or_name="1"), query_string={ - "user": default_user.id_, + "user": user0.id_, "source": "source.txt", "target": "target.txt", "access_token": "wrongtoken", @@ -612,7 +611,7 @@ def test_move_files(app, default_user, _get_user_mock): res = client.put( url_for("workflows.move_files", workflow_id_or_name="1"), query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, "source": "source.txt", "target": "target.txt", }, @@ -627,7 +626,7 @@ def test_move_files(app, default_user, _get_user_mock): ) def test_open_interactive_session( app, - default_user, + user0, sample_serial_workflow_in_db, interactive_session_type, expected_status_code, @@ -645,7 +644,7 @@ def test_open_interactive_session( workflow_id_or_name=sample_serial_workflow_in_db.id_, interactive_session_type=interactive_session_type, ), - query_string={"access_token": default_user.access_token}, + query_string={"access_token": user0.access_token}, ) assert res.status_code == expected_status_code @@ -653,7 +652,7 @@ def test_open_interactive_session( @pytest.mark.parametrize(("expected_status_code"), [200]) def test_close_interactive_session( app, - default_user, + user0, sample_serial_workflow_in_db, expected_status_code, _get_user_mock, @@ -669,7 +668,7 @@ def test_close_interactive_session( "workflows.close_interactive_session", workflow_id_or_name=sample_serial_workflow_in_db.id_, ), - query_string={"access_token": default_user.access_token}, + query_string={"access_token": user0.access_token}, ) assert res.status_code == expected_status_code @@ -710,7 +709,7 @@ def test_create_and_associate_local_user(app, session): assert user.username == mock_user.email -def test_get_workflow_retention_rules(app, default_user): +def test_get_workflow_retention_rules(app, user0): """Test get_workflow_retention_rules.""" endpoint_url = url_for( "workflows.get_workflow_retention_rules", workflow_id_or_name="workflow" @@ -735,13 +734,13 @@ def test_get_workflow_retention_rules(app, default_user): ), ): res = client.get( - endpoint_url, query_string={"access_token": default_user.access_token} + endpoint_url, query_string={"access_token": user0.access_token} ) assert res.status_code == status_code assert "message" in res.json -def test_prune_workspace(app, default_user, sample_serial_workflow_in_db): +def test_prune_workspace(app, user0, sample_serial_workflow_in_db): """Test prune_workspace.""" endpoint_url = url_for( "workflows.prune_workspace", @@ -762,14 +761,14 @@ def test_prune_workspace(app, default_user, sample_serial_workflow_in_db): "workflows.prune_workspace", workflow_id_or_name="invalid_wf", ), - query_string={"access_token": default_user.access_token}, + query_string={"access_token": user0.access_token}, ) assert res.status_code == 403 # Test normal behaviour status_code = 200 res = client.post( - endpoint_url, query_string={"access_token": default_user.access_token} + endpoint_url, query_string={"access_token": user0.access_token} ) assert res.status_code == status_code assert "The workspace has been correctly pruned." in res.json["message"] @@ -777,7 +776,7 @@ def test_prune_workspace(app, default_user, sample_serial_workflow_in_db): res = client.post( endpoint_url, query_string={ - "access_token": default_user.access_token, + "access_token": user0.access_token, "include_inputs": True, "include_outputs": True, }, From 5be444fd7f7d49a1f9021a3569589ec45ff24f1c Mon Sep 17 00:00:00 2001 From: Daan Rosendal Date: Wed, 8 Nov 2023 23:26:03 +0100 Subject: [PATCH 3/4] rest: add unshare_workflow endpoint Adds a new endpoint to unshare a workflow. Closes reanahub/reana-client#681 --- docs/openapi.json | 183 +++++++++++++++++++++++++++++++++ reana_server/rest/workflows.py | 172 +++++++++++++++++++++++++++++++ 2 files changed, 355 insertions(+) diff --git a/docs/openapi.json b/docs/openapi.json index 36d3fad5..4a32e68f 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -3955,6 +3955,189 @@ "summary": "Set status of a workflow." } }, + "/api/workflows/{workflow_id_or_name}/unshare": { + "post": { + "description": "This resource unshares a workflow with another user. This resource is expecting a workflow UUID and some parameters.", + "operationId": "unshare_workflow", + "parameters": [ + { + "description": "The API access_token of workflow owner.", + "in": "query", + "name": "access_token", + "required": false, + "type": "string" + }, + { + "description": "Required. Workflow UUID or name.", + "in": "path", + "name": "workflow_id_or_name", + "required": true, + "type": "string" + }, + { + "description": "Required. User to unshare the workflow with.", + "in": "query", + "name": "user_email_to_unshare_with", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Request succeeded. The workflow has been unshared with the user.", + "examples": { + "application/json": { + "message": "The workflow has been unshared 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 specification seems malformed.", + "examples": { + "application/json": { + "errors": [ + "Missing data for required field." + ], + "message": "Malformed request." + } + }, + "schema": { + "properties": { + "errors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "403": { + "description": "Request failed. User is not allowed to unshare the workflow.", + "examples": { + "application/json": { + "errors": [ + "User is not allowed to unshare the workflow." + ] + } + }, + "schema": { + "properties": { + "errors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "404": { + "description": "Request failed. Workflow does not exist or user does not exist.", + "examples": { + "application/json": { + "errors": [ + "Workflow cdcf48b1-c2f3-4693-8230-b066e088c6ac does not exist" + ], + "message": "Workflow cdcf48b1-c2f3-4693-8230-b066e088c6ac does not exist" + } + }, + "schema": { + "properties": { + "errors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "409": { + "description": "Request failed. The workflow is not shared with the user.", + "examples": { + "application/json": { + "errors": [ + "The workflow is not shared with the user." + ], + "message": "The workflow is not shared with the user." + } + }, + "schema": { + "properties": { + "errors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "500": { + "description": "Request failed. Internal controller error.", + "examples": { + "application/json": { + "errors": [ + "Internal controller error." + ], + "message": "Internal controller error." + } + }, + "schema": { + "properties": { + "errors": { + "items": { + "type": "string" + }, + "type": "array" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "summary": "Unshare a workflow with another user." + } + }, "/api/workflows/{workflow_id_or_name}/workspace": { "get": { "description": "This resource retrieves the file list of a workspace, given its workflow UUID.", diff --git a/reana_server/rest/workflows.py b/reana_server/rest/workflows.py index 2f01f42d..4b0226e7 100644 --- a/reana_server/rest/workflows.py +++ b/reana_server/rest/workflows.py @@ -3436,3 +3436,175 @@ def share_workflow( except Exception as e: logging.exception(str(e)) return jsonify({"message": str(e)}), 500 + + +@blueprint.route("/workflows//unshare", methods=["POST"]) +@use_kwargs( + { + "user_email_to_unshare_with": fields.String(), + } +) +@signin_required() +def unshare_workflow(workflow_id_or_name, user, user_email_to_unshare_with): + r"""Unshare a workflow with another user. + + --- + post: + summary: Unshare a workflow with another user. + description: >- + This resource unshares a workflow with another user. + This resource is expecting a workflow UUID and some parameters. + operationId: unshare_workflow + produces: + - application/json + parameters: + - name: access_token + in: query + description: The API access_token of workflow owner. + required: false + type: string + - name: workflow_id_or_name + in: path + description: Required. Workflow UUID or name. + required: true + type: string + - name: user_email_to_unshare_with + in: query + description: >- + Required. User to unshare the workflow with. + required: true + type: string + responses: + 200: + description: >- + Request succeeded. The workflow has been unshared 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 unshared with the user.", + "workflow_id": "cdcf48b1-c2f3-4693-8230-b066e088c6ac", + "workflow_name": "mytest.1" + } + 400: + description: >- + Request failed. The incoming data specification seems malformed. + schema: + type: object + properties: + message: + type: string + errors: + type: array + items: + type: string + examples: + application/json: + { + "message": "Malformed request.", + "errors": ["Missing data for required field."] + } + 403: + description: >- + Request failed. User is not allowed to unshare the workflow. + schema: + type: object + properties: + message: + type: string + errors: + type: array + items: + type: string + examples: + application/json: + { + "errors": ["User is not allowed to unshare the workflow."] + } + 404: + description: >- + Request failed. Workflow does not exist or user does not exist. + schema: + type: object + properties: + message: + type: string + errors: + type: array + items: + type: string + examples: + application/json: + { + "message": "Workflow cdcf48b1-c2f3-4693-8230-b066e088c6ac does + not exist", + "errors": ["Workflow cdcf48b1-c2f3-4693-8230-b066e088c6ac does + not exist"] + } + 409: + description: >- + Request failed. The workflow is not shared with the user. + schema: + type: object + properties: + message: + type: string + errors: + type: array + items: + type: string + examples: + application/json: + { + "message": "The workflow is not shared with the user.", + "errors": ["The workflow is not shared with the user."] + } + 500: + description: >- + Request failed. Internal controller error. + schema: + type: object + properties: + message: + type: string + errors: + type: array + items: + type: string + examples: + application/json: + { + "message": "Internal controller error.", + "errors": ["Internal controller error."] + } + """ + try: + unshare_params = { + "workflow_id_or_name": workflow_id_or_name, + "user_email_to_unshare_with": user_email_to_unshare_with, + "user_id": str(user.id_), + } + + response, http_response = current_rwc_api_client.api.unshare_workflow( + **unshare_params + ).result() + + return jsonify(response), 200 + except HTTPError as e: + logging.exception(str(e)) + return jsonify(e.response.json()), e.response.status_code + except ValueError as e: + # In case of invalid workflow name / UUID + logging.exception(str(e)) + return jsonify({"message": str(e)}), 403 + except Exception as e: + logging.exception(str(e)) + return jsonify({"message": str(e)}), 500 From 507700f69dae0c9b3a00d5eba1306833ade0a580 Mon Sep 17 00:00:00 2001 From: Daan Rosendal Date: Tue, 14 Nov 2023 22:40:07 +0100 Subject: [PATCH 4/4] rest: add get_workflow_share_status endpoint Adds a new endpoint to retrieve whom a workflow is shared with. Closes reanahub/reana-client#686 --- docs/openapi.json | 133 +++++++++++++++++++++++++++++++++ reana_server/rest/workflows.py | 129 ++++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+) diff --git a/docs/openapi.json b/docs/openapi.json index 4a32e68f..af777d45 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -3163,6 +3163,139 @@ "summary": "Share a workflow with another user." } }, + "/api/workflows/{workflow_id_or_name}/share-status": { + "get": { + "description": "This resource returns the share status of a given workflow.", + "operationId": "get_workflow_share_status", + "parameters": [ + { + "description": "The API access_token of workflow owner.", + "in": "query", + "name": "access_token", + "required": false, + "type": "string" + }, + { + "description": "Required. Workflow UUID or name.", + "in": "path", + "name": "workflow_id_or_name", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Request succeeded. The response contains the share status of the workflow.", + "examples": { + "application/json": { + "shared_with": [ + { + "user_email": "bob@example.org", + "valid_until": "2022-11-24T23:59:59" + } + ], + "workflow_id": "256b25f4-4cfb-4684-b7a8-73872ef455a1", + "workflow_name": "mytest.1" + } + }, + "schema": { + "properties": { + "shared_with": { + "items": { + "properties": { + "user_email": { + "type": "string" + }, + "valid_until": { + "type": "string", + "x-nullable": true + } + }, + "type": "object" + }, + "type": "array" + }, + "workflow_id": { + "type": "string" + }, + "workflow_name": { + "type": "string" + } + }, + "type": "object" + } + }, + "401": { + "description": "Request failed. User not signed in.", + "examples": { + "application/json": { + "message": "User not signed in." + } + }, + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "403": { + "description": "Request failed. Credentials are invalid or revoked.", + "examples": { + "application/json": { + "message": "Token not valid." + } + }, + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "404": { + "description": "Request failed. Workflow does not exist.", + "examples": { + "application/json": { + "message": "Workflow mytest.1 does not exist." + } + }, + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "500": { + "description": "Request failed. Internal server error.", + "examples": { + "application/json": { + "message": "Something went wrong." + } + }, + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "summary": "Get the share status of a workflow." + } + }, "/api/workflows/{workflow_id_or_name}/specification": { "get": { "description": "This resource returns the REANA workflow specification used to start the workflow run. Resource is expecting a workflow UUID.", diff --git a/reana_server/rest/workflows.py b/reana_server/rest/workflows.py index 4b0226e7..c7d7662c 100644 --- a/reana_server/rest/workflows.py +++ b/reana_server/rest/workflows.py @@ -3608,3 +3608,132 @@ def unshare_workflow(workflow_id_or_name, user, user_email_to_unshare_with): except Exception as e: logging.exception(str(e)) return jsonify({"message": str(e)}), 500 + + +@blueprint.route("/workflows//share-status", methods=["GET"]) +@signin_required() +def get_workflow_share_status(workflow_id_or_name, user): + r"""Get the share status of a workflow. + + --- + get: + summary: Get the share status of a workflow. + description: >- + This resource returns the share status of a given workflow. + operationId: get_workflow_share_status + produces: + - application/json + parameters: + - name: access_token + in: query + description: The API access_token of workflow owner. + required: false + type: string + - name: workflow_id_or_name + in: path + description: Required. Workflow UUID or name. + required: true + type: string + responses: + 200: + description: >- + Request succeeded. The response contains the share status of the workflow. + schema: + type: object + properties: + workflow_id: + type: string + workflow_name: + type: string + shared_with: + type: array + items: + type: object + properties: + user_email: + type: string + valid_until: + type: string + x-nullable: true + examples: + application/json: + { + "workflow_id": "256b25f4-4cfb-4684-b7a8-73872ef455a1", + "workflow_name": "mytest.1", + "shared_with": [ + { + "user_email": "bob@example.org", + "valid_until": "2022-11-24T23:59:59" + } + ] + } + 401: + description: >- + Request failed. User not signed in. + schema: + type: object + properties: + message: + type: string + examples: + application/json: + { + "message": "User not signed in." + } + 403: + description: >- + Request failed. Credentials are invalid or revoked. + schema: + type: object + properties: + message: + type: string + examples: + application/json: + { + "message": "Token not valid." + } + 404: + description: >- + Request failed. Workflow does not exist. + schema: + type: object + properties: + message: + type: string + examples: + application/json: + { + "message": "Workflow mytest.1 does not exist." + } + 500: + description: >- + Request failed. Internal server error. + schema: + type: object + properties: + message: + type: string + examples: + application/json: + { + "message": "Something went wrong." + } + """ + try: + share_status_params = { + "workflow_id_or_name": workflow_id_or_name, + "user_id": str(user.id_), + } + + response, http_response = current_rwc_api_client.api.get_workflow_share_status( + **share_status_params + ).result() + + return jsonify(response), 200 + except HTTPError as e: + logging.exception(str(e)) + return jsonify(e.response.json()), e.response.status_code + except Exception as e: + logging.exception(str(e)) + return jsonify({"message": str(e)}), 500