diff --git a/docs/openapi.json b/docs/openapi.json index 01009626..1e6970b0 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1242,6 +1242,216 @@ "summary": "Requests a new access token for the authenticated user." } }, + "/api/users/shared-with-you": { + "get": { + "description": "This resource provides information about users that shared workflow(s) with the authenticated user.", + "operationId": "get_users_shared_with_you", + "parameters": [ + { + "description": "API access_token of user.", + "in": "query", + "name": "access_token", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Users that shared workflow(s) with the authenticated user.", + "examples": { + "application/json": { + "users_shared_with_you": [ + { + "email": "john.doe@example.org", + "full_name": "John Doe", + "username": "jdoe" + } + ] + } + }, + "schema": { + "properties": { + "users": { + "items": { + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + } + }, + "401": { + "description": "Error message indicating that the uses is not authenticated.", + "examples": { + "application/json": { + "message": "User not logged in" + } + }, + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "403": { + "description": "Request failed. User token not valid.", + "examples": { + "application/json": { + "message": "Token is not valid." + } + }, + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "500": { + "description": "Request failed. Internal server error.", + "examples": { + "application/json": { + "message": "Internal server error." + } + }, + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "summary": "Gets users that shared workflow(s) with the authenticated user." + } + }, + "/api/users/you-shared-with": { + "get": { + "description": "This resource provides information about users that the authenticated user shared workflow(s) with.", + "operationId": "get_users_you_shared_with", + "parameters": [ + { + "description": "API access_token of user.", + "in": "query", + "name": "access_token", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Users that the authenticated user shared workflow(s) with.", + "examples": { + "application/json": { + "users_you_shared_with": [ + { + "email": "john.doe@example.org", + "full_name": "John Doe", + "username": "jdoe" + } + ] + } + }, + "schema": { + "properties": { + "users": { + "items": { + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + } + }, + "401": { + "description": "Error message indicating that the uses is not authenticated.", + "examples": { + "application/json": { + "message": "User not logged in" + } + }, + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "403": { + "description": "Request failed. User token not valid.", + "examples": { + "application/json": { + "message": "Token is not valid." + } + }, + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "500": { + "description": "Request failed. Internal server error.", + "examples": { + "application/json": { + "message": "Internal server error." + } + }, + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "summary": "Gets users that the authenticated user shared workflow(s) with." + } + }, "/api/workflows": { "get": { "description": "This resource return all current workflows in JSON format.", diff --git a/reana_server/rest/users.py b/reana_server/rest/users.py index 69290650..f259601f 100644 --- a/reana_server/rest/users.py +++ b/reana_server/rest/users.py @@ -13,7 +13,8 @@ from bravado.exception import HTTPError from flask import Blueprint, jsonify -from reana_db.models import AuditLogAction +from reana_db.database import Session +from reana_db.models import AuditLogAction, User, UserWorkflow, Workflow from reana_commons.config import ( REANA_COMPONENT_PREFIX, REANA_INFRASTRUCTURE_KUBERNETES_NAMESPACE, @@ -354,3 +355,265 @@ def request_token(user): except Exception as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 500 + + +@blueprint.route("/users/shared-with-you", methods=["GET"]) +@signin_required() +def get_users_shared_with_you(user): + r"""Endpoint to get users that shared workflow(s) with the authenticated user. + + --- + get: + summary: Gets users that shared workflow(s) with the authenticated user. + description: >- + This resource provides information about users that shared + workflow(s) with the authenticated user. + operationId: get_users_shared_with_you + produces: + - application/json + parameters: + - name: access_token + in: query + description: API access_token of user. + required: false + type: string + responses: + 200: + description: >- + Users that shared workflow(s) with the authenticated user. + schema: + type: object + properties: + users: + type: array + items: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + examples: + application/json: + { + "users_shared_with_you": [ + { + "email": "john.doe@example.org", + "full_name": "John Doe", + "username": "jdoe" + } + ] + } + 401: + description: >- + Error message indicating that the uses is not authenticated. + schema: + type: object + properties: + message: + type: string + examples: + application/json: + { + "message": "User not logged in" + } + 403: + description: >- + Request failed. User token not valid. + schema: + type: object + properties: + message: + type: string + examples: + application/json: + { + "message": "Token is not valid." + } + 500: + description: >- + Request failed. Internal server error. + schema: + type: object + properties: + message: + type: string + examples: + application/json: + { + "message": "Internal server error." + } + """ + try: + shared_workflows_ids = ( + Session.query(UserWorkflow.workflow_id) + .filter(UserWorkflow.user_id == user.id_) + .subquery() + ) + + shared_workflow_owners_ids = ( + Session.query(Workflow.owner_id) + .filter(Workflow.id_.in_(shared_workflows_ids)) + .distinct() + .subquery() + ) + + users = ( + Session.query(User.email, User.full_name, User.username) + .filter(User.id_.in_(shared_workflow_owners_ids)) + .all() + ) + + response = {"users_shared_with_you": []} + + for email, full_name, username in users: + response["users_shared_with_you"].append( + { + "email": email, + "full_name": full_name, + "username": username, + } + ) + + return jsonify(response), 200 + except HTTPError as e: + logging.error(traceback.format_exc()) + return jsonify(e.response.json()), e.response.status_code + except ValueError as e: + logging.error(traceback.format_exc()) + return jsonify({"message": str(e)}), 403 + except Exception as e: + logging.error(traceback.format_exc()) + return jsonify({"message": str(e)}), 500 + + +@blueprint.route("/users/you-shared-with", methods=["GET"]) +@signin_required() +def get_users_you_shared_with(user): + r"""Endpoint to get users that the authenticated user shared workflow(s) with. + + --- + get: + summary: Gets users that the authenticated user shared workflow(s) with. + description: >- + This resource provides information about users that the authenticated user + shared workflow(s) with. + operationId: get_users_you_shared_with + produces: + - application/json + parameters: + - name: access_token + in: query + description: API access_token of user. + required: false + type: string + responses: + 200: + description: >- + Users that the authenticated user shared workflow(s) with. + schema: + type: object + properties: + users: + type: array + items: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + examples: + application/json: + { + "users_you_shared_with": [ + { + "email": "john.doe@example.org", + "full_name": "John Doe", + "username": "jdoe" + } + ] + } + 401: + description: >- + Error message indicating that the uses is not authenticated. + schema: + type: object + properties: + message: + type: string + examples: + application/json: + { + "message": "User not logged in" + } + 403: + description: >- + Request failed. User token not valid. + schema: + type: object + properties: + message: + type: string + examples: + application/json: + { + "message": "Token is not valid." + } + 500: + description: >- + Request failed. Internal server error. + schema: + type: object + properties: + message: + type: string + examples: + application/json: + { + "message": "Internal server error." + } + """ + try: + owned_workflows_ids = ( + Session.query(Workflow.id_).filter(Workflow.owner_id == user.id_).subquery() + ) + + users_you_shared_with_ids = ( + Session.query(UserWorkflow.user_id) + .filter(UserWorkflow.workflow_id.in_(owned_workflows_ids)) + .distinct() + .subquery() + ) + + users = ( + Session.query(User.email, User.full_name, User.username) + .filter(User.id_.in_(users_you_shared_with_ids)) + .all() + ) + + response = {"users_you_shared_with": []} + + for email, full_name, username in users: + response["users_you_shared_with"].append( + { + "email": email, + "full_name": full_name, + "username": username, + } + ) + + return jsonify(response), 200 + except HTTPError as e: + logging.error(traceback.format_exc()) + return jsonify(e.response.json()), e.response.status_code + except ValueError as e: + logging.error(traceback.format_exc()) + return jsonify({"message": str(e)}), 403 + except Exception as e: + logging.error(traceback.format_exc()) + return jsonify({"message": str(e)}), 500 diff --git a/tests/test_users.py b/tests/test_users.py new file mode 100644 index 00000000..f466f3de --- /dev/null +++ b/tests/test_users.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# +# This file is part of REANA. +# Copyright (C) 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. + +"""Test users endpoints.""" + + +import pytest +from flask import url_for +from mock import patch +from pytest_reana.test_utils import make_mock_api_client + + +def test_get_users_shared_with_you(app, user1): + """Test getting users who shared workflows with you.""" + with app.test_client() as client: + response = client.get( + url_for("users.get_users_shared_with_you"), + ) + + assert response.status_code == 401 + + response = client.get( + url_for("users.get_users_shared_with_you"), + query_string={"access_token": "invalid_token"}, + ) + + assert response.status_code == 403 + + response = client.get( + url_for("users.get_users_shared_with_you"), + query_string={"access_token": user1.access_token}, + ) + + assert response.status_code == 200 + + +def test_get_users_you_shared_with(app, user1): + """Test getting users who you shared workflows with.""" + with app.test_client() as client: + response = client.get( + url_for("users.get_users_you_shared_with"), + ) + + assert response.status_code == 401 + + response = client.get( + url_for("users.get_users_you_shared_with"), + query_string={"access_token": "invalid_token"}, + ) + + assert response.status_code == 403 + + response = client.get( + url_for("users.get_users_you_shared_with"), + query_string={"access_token": user1.access_token}, + ) + + assert response.status_code == 200