From f02ec9e0160dee285671ac50d4dd84b8a7105f2b Mon Sep 17 00:00:00 2001 From: tomondre Date: Tue, 22 Jul 2025 10:38:08 +0200 Subject: [PATCH 1/5] feat: add JWT authorization (#741) Uses JWT via Authlib and JWK's for verification --- reana_server/decorators.py | 6 +++- reana_server/utils.py | 68 ++++++++++++++++++++++++++++++++++++++ requirements.txt | 5 +-- setup.py | 2 ++ 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/reana_server/decorators.py b/reana_server/decorators.py index e3ae651c..4d6fa38b 100644 --- a/reana_server/decorators.py +++ b/reana_server/decorators.py @@ -20,10 +20,11 @@ _get_user_from_invenio_user, get_user_from_token, get_quota_excess_message, + _get_user_from_jwt, ) -def signin_required(include_gitlab_login=False, token_required=True): +def signin_required(include_gitlab_login=False, token_required=True, include_jwt=False): """Check if the user is signed in or the access token is valid and return the user.""" def decorator(func): @@ -37,6 +38,9 @@ def wrapper(*args, **kwargs): user = get_user_from_token(request.headers["X-Gitlab-Token"]) elif "access_token" in request.args: user = get_user_from_token(request.args.get("access_token")) + elif include_jwt and request.headers["Authorization"]: + user = _get_user_from_jwt(request.headers["Authorization"]) + if not user: return jsonify(message="User not signed in"), 401 if token_required and not user.active_token: diff --git a/reana_server/utils.py b/reana_server/utils.py index 8662d77f..05f3b521 100644 --- a/reana_server/utils.py +++ b/reana_server/utils.py @@ -17,6 +17,8 @@ import secrets import sys import shutil +from authlib.jose import jwt, JoseError +from authlib.jose.rfc7517.jwk import JsonWebKey from typing import Any, Dict, List, Optional, Union, Generator from uuid import UUID, uuid4 @@ -433,6 +435,13 @@ def _get_user_from_invenio_user(id): return user +def _get_user_by_sub_and_iss(sub, iss): + user = Session.query(User).filter_by(idp_subject=sub, idp_issuer=iss).one_or_none() + if not user: + raise ValueError("No users registered with this idp_id") + return user + + def _get_reana_yaml_from_gitlab(webhook_data, user_id): reana_yaml = "reana.yaml" if webhook_data["object_kind"] == "push": @@ -658,3 +667,62 @@ def render_template(template_path, **kwargs): """Render template replacing kwargs appropriately.""" template = JinjaEnv._get().get_template(template_path) return template.render(**kwargs) + + +def fetch_and_parse_jwk(): + """Fetch and return specific JWK from an identity provider. + + Returns: + dict: JWK matching the kid + + Raises: + ValueError: If JWK fetch fails or no matching key found + """ + if not hasattr(fetch_and_parse_jwk, "_cache"): + fetch_and_parse_jwk._cache = None + + if not fetch_and_parse_jwk._cache: + response = requests.get("https://iam-escape.cloud.cnaf.infn.it/jwk") + if response.status_code != 200: + raise ValueError(f"Failed to fetch JWK: {response.status_code}") + fetch_and_parse_jwk._cache = response.json() + jwks = fetch_and_parse_jwk._cache.get("keys", []) + if not jwks: + raise ValueError("No JWKs found in the response") + return jwks + + +def _get_user_from_jwt(header: str) -> User: + """Get user from JWT token. + + Args: + header: JWT Authorization header in the format "Bearer " + + Returns: + User: REANA user if token is valid + + Raises: + ValueError: If token is invalid or user not found + """ + try: + if not header.startswith("Bearer "): + raise ValueError("Invalid authorization header format") + + token = header.split(" ")[1] + + jwks = fetch_and_parse_jwk() + key_set = JsonWebKey.import_key_set(jwks) + + claims = jwt.decode(token, key_set) + claims.validate() + + sub = claims.get("sub") + iss = claims.get("iss") + if not sub or not iss: + raise ValueError("Token missing subject claim or iss") + + return _get_user_by_sub_and_iss(sub, iss) + except JoseError as e: + raise ValueError(f"Invalid token: {str(e)}") + except Exception as e: + raise ValueError(f"Error processing token: {str(e)}") diff --git a/requirements.txt b/requirements.txt index 7630a7d5..f48b84c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ argparse-dataclass==2.0.0 # via snakemake-interface-common, snakemake-interface arrow==1.3.0 # via isoduration asttokens==3.0.0 # via stack-data attrs==25.1.0 # via jsonschema +authlib==1.6.0 # via reana-server (setup.py) babel==2.17.0 # via flask-babel, invenio-i18n bagit==1.8.1 # via cwltool billiard==4.2.1 # via celery @@ -37,7 +38,7 @@ commonmark==0.9.1 # via rich conda-inject==1.3.2 # via snakemake configargparse==1.7 # via snakemake, snakemake-interface-common connection-pool==0.0.3 # via snakemake -cryptography==44.0.0 # via invenio-accounts, pyjwt, sqlalchemy-utils +cryptography==44.0.0 # via authlib, invenio-accounts, pyjwt, sqlalchemy-utils cwltool==3.1.20210628163208 # via reana-commons datrie==0.8.2 # via snakemake decorator==5.1.1 # via ipython, jsonpath-rw @@ -166,7 +167,7 @@ pytz==2024.1 # via bravado-core, flask-babel, invenio-i18n pywebpack==2.1.0 # via flask-webpackext pyyaml==6.0.2 # via bravado, bravado-core, conda-inject, kubernetes, packtivity, reana-commons, snakemake, swagger-spec-validator, yadage, yadage-schemas, yte rdflib==5.0.0 # via cwltool, prov, schema-salad -reana-commons[cwl,kubernetes,snakemake,yadage]==0.95.0a7 # via reana-db, reana-server (setup.py) +reana-commons[cwl,kubernetes,snakemake,yadage]==0.95.0a9 # via reana-db, reana-server (setup.py) reana-db==0.95.0a5 # via reana-server (setup.py) redis==5.2.1 # via invenio-celery requests[security]==2.32.3 # via bravado, bravado-core, cachecontrol, cwltool, github3-py, kubernetes, packtivity, reana-server (setup.py), requests-oauthlib, schema-salad, snakemake, yadage, yadage-schemas diff --git a/setup.py b/setup.py index 16717213..d8057d9b 100644 --- a/setup.py +++ b/setup.py @@ -82,6 +82,8 @@ "invenio-theme>=1.4.7", "invenio-i18n>=1.3.3", "invenio-access>=2.0.0", + # JWT Auth support + "authlib>=1.6.0", # Invenio database "invenio-db[postgresql]>=1.0.5", "six>=1.12.0", # required by Flask-Breadcrumbs From d9ce5bee0b4f9a6cc848797514a3483c9fb720d3 Mon Sep 17 00:00:00 2001 From: tomondre Date: Tue, 22 Jul 2025 10:40:02 +0200 Subject: [PATCH 2/5] test: add JWT authorization decorator test cases (#741) --- tests/test_decorators.py | 60 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 8e975b3f..d63c433c 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -8,7 +8,7 @@ """REANA-Server decorators tests.""" import json -from unittest.mock import Mock, patch +from unittest.mock import Mock, patch, MagicMock from flask import jsonify from reana_db.models import User, UserToken @@ -31,3 +31,61 @@ def test_signing_required_with_token(user0: User): mock_endpoint.assert_not_called() assert code == 401 assert error["message"] == "User has no active tokens" + + +def test_signin_required_with_jwt(user0: User): + """Test `signin_required` with JWT token authentication.""" + mock_endpoint = Mock(return_value=(jsonify(message="Success"), 200)) + mock_current_user = Mock() + mock_current_user.is_authenticated = False + mock_current_user.email = user0.email + + mock_request = Mock() + mock_request.headers = {"Authorization": "Bearer token123"} + mock_request.args = {} + mock_request_context = MagicMock() + mock_request_context.__enter__.return_value = mock_request + + with patch("reana_server.decorators._get_user_from_jwt", mock_current_user): + with patch("reana_server.decorators.current_user", mock_current_user): + with patch("reana_server.decorators.request", mock_request): + decorated_endpoint = signin_required(include_jwt=True)(mock_endpoint) + response, code = decorated_endpoint() + + # Should call the endpoint since authentication succeeded + mock_endpoint.assert_called_once() + assert code == 200 + assert ( + json.loads(response.get_data(as_text=True))["message"] == "Success" + ) + + +def test_signin_required_with_invalid_jwt(user0: User): + """Test `signin_required` with invalid JWT token.""" + mock_endpoint = Mock(return_value=(jsonify(message="Success"), 200)) + mock_current_user = Mock() + mock_current_user.is_authenticated = False + mock_current_user.email = user0.email + + mock_request = Mock() + mock_request.headers = {"Authorization": "Bearer invalid_token"} + mock_request.args = {} + mock_request_context = MagicMock() + mock_request_context.__enter__.return_value = mock_request + + with patch("reana_server.decorators.current_user", mock_current_user): + with patch( + "reana_server.decorators._get_user_from_jwt", + side_effect=ValueError("Invalid token"), + ): + with patch("reana_server.decorators.request", mock_request): + decorated_endpoint = signin_required(include_jwt=True)(mock_endpoint) + response, code = decorated_endpoint() + + # Should not call the endpoint since authentication failed + mock_endpoint.assert_not_called() + assert code == 403 + assert ( + json.loads(response.get_data(as_text=True))["message"] + == "Invalid token" + ) From 7491c25820ca13820070ecc00c51cde40ef5cf04 Mon Sep 17 00:00:00 2001 From: tomondre Date: Tue, 22 Jul 2025 12:08:02 +0200 Subject: [PATCH 3/5] feat: add configuration for JWK URL (#741) --- reana_server/config.py | 2 ++ reana_server/utils.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/reana_server/config.py b/reana_server/config.py index 39d7df8a..261bbfe7 100644 --- a/reana_server/config.py +++ b/reana_server/config.py @@ -346,6 +346,8 @@ def _get_rate_limit(env_variable: str, default: str) -> str: OAUTHCLIENT_REMOTE_APPS["keycloak"] = KEYCLOAK_APP OAUTHCLIENT_REST_REMOTE_APPS["keycloak"] = KEYCLOAK_REST_APP + REANA_OAUTH_JWK_URL = PROVIDER_CONFIG.get("jwk_url", "") + # CERN SSO configuration OAUTH_REMOTE_REST_APP = copy.deepcopy(cern_openid.REMOTE_REST_APP) OAUTH_REMOTE_REST_APP.update( diff --git a/reana_server/utils.py b/reana_server/utils.py index 05f3b521..c93ab247 100644 --- a/reana_server/utils.py +++ b/reana_server/utils.py @@ -75,6 +75,7 @@ REANA_QUOTAS_DOCS_URL, WORKSPACE_RETENTION_PERIOD, DEFAULT_WORKSPACE_RETENTION_RULE, + REANA_OAUTH_JWK_URL, ) from reana_server.gitlab_client import ( GitLabClient, @@ -682,7 +683,7 @@ def fetch_and_parse_jwk(): fetch_and_parse_jwk._cache = None if not fetch_and_parse_jwk._cache: - response = requests.get("https://iam-escape.cloud.cnaf.infn.it/jwk") + response = requests.get(REANA_OAUTH_JWK_URL) if response.status_code != 200: raise ValueError(f"Failed to fetch JWK: {response.status_code}") fetch_and_parse_jwk._cache = response.json() From 3026da6be179dd0aab0b9c519b1f0c713cde213c Mon Sep 17 00:00:00 2001 From: tomondre Date: Tue, 22 Jul 2025 15:39:36 +0200 Subject: [PATCH 4/5] feat(docs): add Authorization header for JWT in API specification (#741) --- docs/openapi.json | 226 ++++++++++++++++++++++++++++++++- reana_server/config.py | 3 + reana_server/rest/config.py | 5 + reana_server/rest/gitlab.py | 5 + reana_server/rest/info.py | 7 +- reana_server/rest/secrets.py | 15 +++ reana_server/rest/users.py | 20 +++ reana_server/rest/workflows.py | 110 ++++++++++++++++ 8 files changed, 389 insertions(+), 2 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index 9165e631..3d9012fc 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -18,6 +18,13 @@ "name": "access_token", "required": false, "type": "string" + }, + { + "description": "The JWT of user.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" } ], "produces": [ @@ -152,6 +159,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the current user.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "The search string to filter the project list.", "in": "query", @@ -409,7 +423,14 @@ "description": "The API access_token of workflow owner.", "in": "query", "name": "access_token", - "required": true, + "required": false, + "type": "string" + }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, "type": "string" } ], @@ -1043,6 +1064,13 @@ "name": "access_token", "required": false, "type": "string" + }, + { + "description": "The JWT of secrets owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" } ], "produces": [ @@ -1131,6 +1159,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the admin.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Optional. List of secrets to be deleted.", "in": "body", @@ -1230,6 +1265,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of secrets owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Whether existing secret keys should be overwritten.", "in": "query", @@ -1508,6 +1550,13 @@ "name": "access_token", "required": false, "type": "string" + }, + { + "description": "The JWT of the current user.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" } ], "produces": [ @@ -1604,6 +1653,13 @@ "name": "access_token", "required": false, "type": "string" + }, + { + "description": "The JWT of the current user.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" } ], "produces": [ @@ -1701,6 +1757,13 @@ "name": "access_token", "required": false, "type": "string" + }, + { + "description": "The JWT of current user.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" } ], "produces": [ @@ -1799,6 +1862,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Required. Type of workflows.", "in": "query", @@ -2195,6 +2265,13 @@ "name": "access_token", "required": false, "type": "string" + }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" } ], "produces": [ @@ -2331,6 +2408,13 @@ "name": "access_token", "required": false, "type": "string" + }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" } ], "produces": [ @@ -2486,6 +2570,13 @@ "name": "access_token", "required": false, "type": "string" + }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" } ], "produces": [ @@ -2605,6 +2696,13 @@ "name": "access_token", "required": false, "type": "string" + }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" } ], "produces": [ @@ -2707,6 +2805,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Required. Analysis UUID or name.", "in": "path", @@ -2877,6 +2982,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Required. Analysis UUID or name.", "in": "path", @@ -3039,6 +3151,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Type of interactive session to use.", "in": "path", @@ -3168,6 +3287,13 @@ "name": "access_token", "required": false, "type": "string" + }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" } ], "produces": [ @@ -3288,6 +3414,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Required. Analysis UUID or name.", "in": "path", @@ -3418,6 +3551,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Required. Analysis UUID or name.", "in": "path", @@ -3563,6 +3703,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Required. Workflow UUID or name.", "in": "path", @@ -3732,6 +3879,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Required. Workflow UUID or name.", "in": "path", @@ -3865,6 +4019,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Required. Analysis UUID or name.", "in": "path", @@ -4071,6 +4232,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Optional. Additional input parameters and operational options.", "in": "body", @@ -4253,6 +4421,13 @@ "name": "access_token", "required": false, "type": "string" + }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" } ], "produces": [ @@ -4497,6 +4672,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Optional. Additional parameters to customise the workflow status change.", "in": "body", @@ -4677,6 +4859,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Required. Workflow UUID or name.", "in": "path", @@ -4823,6 +5012,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "File name(s) (glob) to list.", "in": "query", @@ -4996,6 +5192,13 @@ "required": false, "type": "string" }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" + }, { "description": "Optional flag to return a previewable response of the file (corresponding mime-type).", "in": "query", @@ -5112,6 +5315,13 @@ "name": "access_token", "required": false, "type": "string" + }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" } ], "produces": [ @@ -5223,6 +5433,13 @@ "name": "access_token", "required": false, "type": "string" + }, + { + "description": "The JWT of the workflow owner.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" } ], "produces": [ @@ -5313,6 +5530,13 @@ "name": "access_token", "required": false, "type": "string" + }, + { + "description": "The JWT of the current user.", + "in": "header", + "name": "Authorization", + "required": false, + "type": "string" } ], "produces": [ diff --git a/reana_server/config.py b/reana_server/config.py index 261bbfe7..773bce56 100644 --- a/reana_server/config.py +++ b/reana_server/config.py @@ -302,6 +302,9 @@ def _get_rate_limit(env_variable: str, default: str) -> str: OAUTHCLIENT_REMOTE_APPS = dict() OAUTHCLIENT_REST_REMOTE_APPS = dict() +# Default value for when no login providers are configured. Used for JWT validation. +REANA_OAUTH_JWK_URL = None + # Keycloak is only configured if login providers are defined if REANA_SSO_LOGIN_PROVIDERS: # Variables for the first login provider in the JSON diff --git a/reana_server/rest/config.py b/reana_server/rest/config.py index daa7c901..1a8c7b14 100644 --- a/reana_server/rest/config.py +++ b/reana_server/rest/config.py @@ -36,6 +36,11 @@ def get_config(): description: API access_token of user. required: false type: string + - name: Authorization + in: header + description: The JWT of user. + required: false + type: string responses: 200: description: >- diff --git a/reana_server/rest/gitlab.py b/reana_server/rest/gitlab.py index 1a73ee94..9f7fceb3 100644 --- a/reana_server/rest/gitlab.py +++ b/reana_server/rest/gitlab.py @@ -227,6 +227,11 @@ def gitlab_projects( description: The API access_token of the current user. required: false type: string + - name: Authorization + in: header + description: The JWT of the current user. + required: false + type: string - name: search in: query description: The search string to filter the project list. diff --git a/reana_server/rest/info.py b/reana_server/rest/info.py index 4e51c12e..cc2f2033 100644 --- a/reana_server/rest/info.py +++ b/reana_server/rest/info.py @@ -59,7 +59,12 @@ def info(user, **kwargs): # noqa - name: access_token in: query description: The API access_token of workflow owner. - required: true + required: false + type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false type: string responses: 200: diff --git a/reana_server/rest/secrets.py b/reana_server/rest/secrets.py index 70850b65..89dfb71f 100644 --- a/reana_server/rest/secrets.py +++ b/reana_server/rest/secrets.py @@ -68,6 +68,11 @@ def add_secrets(user, overwrite=False): description: Secrets owner access token. required: false type: string + - name: Authorization + in: header + description: The JWT of secrets owner. + required: false + type: string - name: overwrite in: query description: Whether existing secret keys should be overwritten. @@ -199,6 +204,11 @@ def get_secrets(user): # noqa description: Secrets owner access token. required: false type: string + - name: Authorization + in: header + description: The JWT of secrets owner. + required: false + type: string responses: 200: description: >- @@ -297,6 +307,11 @@ def delete_secrets(user): # noqa description: API key of the admin. required: false type: string + - name: Authorization + in: header + description: The JWT of the admin. + required: false + type: string - name: secrets in: body description: >- diff --git a/reana_server/rest/users.py b/reana_server/rest/users.py index 6efaf67c..a1b06fb7 100644 --- a/reana_server/rest/users.py +++ b/reana_server/rest/users.py @@ -51,6 +51,11 @@ def get_you(user): description: API access_token of user. required: false type: string + - name: Authorization + in: header + description: The JWT of the current user. + required: false + type: string responses: 200: description: >- @@ -246,6 +251,11 @@ def request_token(user): description: API access_token of user. required: false type: string + - name: Authorization + in: header + description: The JWT of the current user. + required: false + type: string responses: 200: description: >- @@ -378,6 +388,11 @@ def get_users_shared_with_you(user): description: API access_token of user. required: false type: string + - name: Authorization + in: header + description: The JWT of the current user. + required: false + type: string responses: 200: description: >- @@ -493,6 +508,11 @@ def get_users_you_shared_with(user): description: API access_token of user. required: false type: string + - name: Authorization + in: header + description: The JWT of current user. + required: false + type: string responses: 200: description: >- diff --git a/reana_server/rest/workflows.py b/reana_server/rest/workflows.py index 419e38a0..a54d3e34 100644 --- a/reana_server/rest/workflows.py +++ b/reana_server/rest/workflows.py @@ -93,6 +93,11 @@ def get_workflows(user, **kwargs): # noqa description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string - name: type in: query description: Required. Type of workflows. @@ -435,6 +440,11 @@ def create_workflow(user): # noqa description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string responses: 201: description: >- @@ -651,6 +661,11 @@ def get_workflow_specification(workflow_id_or_name, user): # noqa description: API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string - name: workflow_id_or_name in: path description: Required. Analysis UUID or name. @@ -846,6 +861,11 @@ def get_workflow_logs(workflow_id_or_name, user, **kwargs): # noqa description: API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string - name: workflow_id_or_name in: path description: Required. Analysis UUID or name. @@ -1003,6 +1023,11 @@ def get_workflow_status(workflow_id_or_name, user): # noqa description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string responses: 200: description: >- @@ -1276,6 +1301,11 @@ def start_workflow(workflow_id_or_name, user, **parameters): # noqa description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string - name: parameters in: body description: >- @@ -1463,6 +1493,11 @@ def set_workflow_status(workflow_id_or_name, user, status, **parameters): # noq description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string - name: parameters in: body description: >- @@ -1684,6 +1719,11 @@ def upload_file(workflow_id_or_name, user): # noqa description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string - name: preview in: query description: >- @@ -1840,6 +1880,11 @@ def download_file(workflow_id_or_name, file_name, user): # noqa description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string responses: 200: description: >- @@ -1962,6 +2007,11 @@ def delete_file(workflow_id_or_name, file_name, user): # noqa description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string responses: 200: description: >- @@ -2080,6 +2130,11 @@ def get_files(workflow_id_or_name, user, **kwargs): # noqa description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string - name: file_name in: query description: File name(s) (glob) to list. @@ -2228,6 +2283,11 @@ def get_workflow_parameters(workflow_id_or_name, user): # noqa description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string responses: 200: description: >- @@ -2379,6 +2439,11 @@ def get_workflow_diff(workflow_id_or_name_a, workflow_id_or_name_b, user): # no description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string responses: 200: description: >- @@ -2516,6 +2581,11 @@ def open_interactive_session( description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string - name: interactive_session_type in: path description: Type of interactive session to use. @@ -2669,6 +2739,11 @@ def close_interactive_session(workflow_id_or_name, user): # noqa description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string responses: 200: description: >- @@ -2796,6 +2871,11 @@ def move_files(workflow_id_or_name, user): # noqa description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string responses: 200: description: >- @@ -2930,6 +3010,11 @@ def get_workflow_disk_usage(workflow_id_or_name, user): # noqa description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string - name: workflow_id_or_name in: path description: Required. Analysis UUID or name. @@ -3100,6 +3185,11 @@ def get_workflow_retention_rules(workflow_id_or_name, user): description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string - name: workflow_id_or_name in: path description: Required. Analysis UUID or name. @@ -3246,6 +3336,11 @@ def prune_workspace( description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string - name: workflow_id_or_name in: path description: Required. Analysis UUID or name. @@ -3397,6 +3492,11 @@ def share_workflow(workflow_id_or_name, user, **kwargs): description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string - name: workflow_id_or_name in: path description: Required. Workflow UUID or name. @@ -3555,6 +3655,11 @@ def unshare_workflow(workflow_id_or_name, user, user_email_to_unshare_with): description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string - name: workflow_id_or_name in: path description: Required. Workflow UUID or name. @@ -3696,6 +3801,11 @@ def get_workflow_share_status(workflow_id_or_name, user): description: The API access_token of workflow owner. required: false type: string + - name: Authorization + in: header + description: The JWT of the workflow owner. + required: false + type: string - name: workflow_id_or_name in: path description: Required. Workflow UUID or name. From 9f8cac69a85faebfe7e722f7ee7f5626811b26e8 Mon Sep 17 00:00:00 2001 From: tomondre Date: Wed, 20 Aug 2025 13:51:46 +0200 Subject: [PATCH 5/5] feat(auth): remove `include_jwt` for full api auth (#745) --- reana_server/decorators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reana_server/decorators.py b/reana_server/decorators.py index 4d6fa38b..63397b66 100644 --- a/reana_server/decorators.py +++ b/reana_server/decorators.py @@ -24,7 +24,7 @@ ) -def signin_required(include_gitlab_login=False, token_required=True, include_jwt=False): +def signin_required(include_gitlab_login=False, token_required=True): """Check if the user is signed in or the access token is valid and return the user.""" def decorator(func): @@ -38,7 +38,7 @@ def wrapper(*args, **kwargs): user = get_user_from_token(request.headers["X-Gitlab-Token"]) elif "access_token" in request.args: user = get_user_from_token(request.args.get("access_token")) - elif include_jwt and request.headers["Authorization"]: + elif request.headers["Authorization"]: user = _get_user_from_jwt(request.headers["Authorization"]) if not user: